Skip to main content

dissolve_python/
ruff_parser.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//! Python parser and CST manipulation using Ruff's parser.
16//!
17//! This module provides a Rust implementation that preserves formatting
18//! and integrates with mypy for type inference.
19
20use anyhow::{anyhow, Result};
21use rustpython_ast::{ExceptHandler as AstExceptHandler, Expr as AstExpr, Ranged, Stmt as AstStmt};
22use rustpython_parser::{parse, Mode};
23use std::collections::HashMap;
24use std::ops::Range;
25
26use crate::core::{CollectorResult, ReplaceInfo};
27use crate::types::TypeIntrospectionMethod;
28
29/// Parse Python source code preserving all formatting information
30pub struct PythonModule<'a> {
31    source: &'a str,
32    ast: Vec<AstStmt>,
33    /// Map from byte offset to line/column for mypy integration
34    position_map: HashMap<u32, (u32, u32)>,
35}
36
37impl<'a> PythonModule<'a> {
38    /// Parse Python source code
39    pub fn parse(source: &'a str) -> Result<Self> {
40        let parsed =
41            parse(source, Mode::Module, "<module>").map_err(|e| anyhow!("Parse error: {:?}", e))?;
42
43        // Convert Mod to Suite
44        let ast = match parsed {
45            rustpython_ast::Mod::Module(module) => module.body,
46            _ => return Err(anyhow!("Expected Module, got Expression")),
47        };
48
49        // Build position map for byte offset -> line/column conversion
50        let position_map = Self::build_position_map(source);
51
52        Ok(Self {
53            source,
54            ast,
55            position_map,
56        })
57    }
58
59    /// Build a map from byte offset to (line, column)
60    fn build_position_map(source: &str) -> HashMap<u32, (u32, u32)> {
61        let mut map = HashMap::new();
62        let mut line = 1;
63        let mut col = 0;
64
65        for (offset, ch) in source.char_indices() {
66            map.insert(offset as u32, (line, col));
67            if ch == '\n' {
68                line += 1;
69                col = 0;
70            } else {
71                col += 1;
72            }
73        }
74
75        // Add end position
76        map.insert(source.len() as u32, (line, col));
77
78        map
79    }
80
81    /// Get AST
82    pub fn ast(&self) -> &[AstStmt] {
83        &self.ast
84    }
85
86    /// Convert byte offset to line/column for mypy
87    pub fn offset_to_position(&self, offset: usize) -> Option<(u32, u32)> {
88        self.position_map.get(&(offset as u32)).copied()
89    }
90
91    /// Convert byte offset to line/column (alias for compatibility)
92    pub fn line_col_at_offset(&self, offset: usize) -> (u32, u32) {
93        self.offset_to_position(offset).unwrap_or((1, 0))
94    }
95
96    /// Get text for a range
97    pub fn text_at_range(&self, range: Range<usize>) -> &str {
98        &self.source[range]
99    }
100}
101
102/// Collect deprecated functions using Ruff's AST
103/// For now, we delegate to LibCST collector until we implement full extraction
104pub fn collect_deprecated_functions(_source: &str, _module_name: &str) -> Result<CollectorResult> {
105    // TODO: Complete rustpython migration
106    // For now, return empty result
107    Ok(CollectorResult {
108        replacements: HashMap::new(),
109        unreplaceable: HashMap::new(),
110        imports: Vec::new(),
111        inheritance_map: HashMap::new(),
112        class_methods: HashMap::new(),
113    })
114}
115
116/// Visitor to find and replace function calls
117pub struct FunctionCallReplacer<'a> {
118    replacements_info: HashMap<String, ReplaceInfo>,
119    replacements: Vec<(Range<usize>, String)>,
120    #[allow(dead_code)]
121    source_module: &'a PythonModule<'a>,
122    #[allow(dead_code)]
123    type_introspection: TypeIntrospectionMethod,
124    #[allow(dead_code)]
125    file_path: String,
126    #[allow(dead_code)]
127    module_name: String,
128    import_map: HashMap<String, String>, // Maps imported names to their full module paths
129}
130
131impl<'a> FunctionCallReplacer<'a> {
132    pub fn new(
133        replacements: HashMap<String, ReplaceInfo>,
134        source_module: &'a PythonModule<'a>,
135        type_introspection: TypeIntrospectionMethod,
136        file_path: String,
137        module_name: String,
138    ) -> Self {
139        let mut replacer = Self {
140            replacements_info: replacements,
141            replacements: Vec::new(),
142            source_module,
143            type_introspection,
144            file_path,
145            module_name,
146            import_map: HashMap::new(),
147        };
148        // Collect imports from the module
149        replacer.collect_imports();
150        replacer
151    }
152
153    pub fn get_replacements(self) -> Vec<(Range<usize>, String)> {
154        self.replacements
155    }
156
157    /// Collect import statements and build the import map
158    fn collect_imports(&mut self) {
159        for stmt in self.source_module.ast() {
160            match stmt {
161                AstStmt::Import(import) => {
162                    // Handle: import module.submodule as alias
163                    for alias in &import.names {
164                        let module_name = alias.name.to_string();
165                        let alias_name = alias
166                            .asname
167                            .as_ref()
168                            .map(|id| id.to_string())
169                            .unwrap_or_else(|| module_name.clone());
170                        self.import_map.insert(alias_name, module_name);
171                    }
172                }
173                AstStmt::ImportFrom(import_from) => {
174                    if let Some(module) = &import_from.module {
175                        let module_str = module.to_string();
176                        // Handle: from module import name as alias
177                        for alias in &import_from.names {
178                            let imported_name = alias.name.to_string();
179                            let alias_name = alias
180                                .asname
181                                .as_ref()
182                                .map(|id| id.to_string())
183                                .unwrap_or_else(|| imported_name.clone());
184
185                            // Only track imports without aliases for now
186                            // Alias tracking would require more complex import rewriting
187                            if alias.asname.is_none() {
188                                let full_name = format!("{}.{}", module_str, imported_name);
189                                self.import_map.insert(alias_name, full_name);
190                            }
191                        }
192                    }
193                }
194                _ => {}
195            }
196        }
197    }
198}
199
200impl<'a> FunctionCallReplacer<'a> {
201    pub fn visit_expr(&mut self, expr: &'a AstExpr) {
202        match expr {
203            AstExpr::Call(call) => {
204                // Check if this is a magic method call (int, repr, bool, len, etc.)
205                if let Some(replacement) = self.try_replace_magic_method_call(call) {
206                    let range = expr.range();
207                    let byte_range = std::ops::Range {
208                        start: usize::from(range.start()),
209                        end: usize::from(range.end()),
210                    };
211                    self.replacements.push((byte_range, replacement));
212                } else if let Some(func_name) = self.get_function_name(&call.func) {
213                    // Check if this is a regular function call we need to replace
214                    if let Some(replace_info) = self.replacements_info.get(&func_name) {
215                        // We found a function to replace
216                        // Build the replacement with actual arguments
217                        let replacement = self.build_replacement(replace_info, call);
218
219                        // Get the range of the entire call expression using the Ranged trait
220                        let range = expr.range();
221                        let byte_range = std::ops::Range {
222                            start: usize::from(range.start()),
223                            end: usize::from(range.end()),
224                        };
225
226                        self.replacements.push((byte_range, replacement));
227                    }
228                }
229
230                // Visit arguments recursively
231                for arg in &call.args {
232                    self.visit_expr(arg);
233                }
234            }
235            AstExpr::Attribute(attr) => {
236                self.visit_expr(&attr.value);
237            }
238            AstExpr::BinOp(binop) => {
239                self.visit_expr(&binop.left);
240                self.visit_expr(&binop.right);
241            }
242            AstExpr::UnaryOp(unaryop) => {
243                self.visit_expr(&unaryop.operand);
244            }
245            AstExpr::IfExp(ifexp) => {
246                self.visit_expr(&ifexp.test);
247                self.visit_expr(&ifexp.body);
248                self.visit_expr(&ifexp.orelse);
249            }
250            AstExpr::Dict(dict) => {
251                for key in dict.keys.iter().flatten() {
252                    self.visit_expr(key);
253                }
254                for value in &dict.values {
255                    self.visit_expr(value);
256                }
257            }
258            AstExpr::Set(set) => {
259                for elt in &set.elts {
260                    self.visit_expr(elt);
261                }
262            }
263            AstExpr::ListComp(comp) => {
264                self.visit_expr(&comp.elt);
265                // Would need to visit generators too
266            }
267            AstExpr::SetComp(comp) => {
268                self.visit_expr(&comp.elt);
269                // Would need to visit generators too
270            }
271            AstExpr::DictComp(comp) => {
272                self.visit_expr(&comp.key);
273                self.visit_expr(&comp.value);
274                // Would need to visit generators too
275            }
276            AstExpr::GeneratorExp(gen) => {
277                self.visit_expr(&gen.elt);
278                // Would need to visit generators too
279            }
280            AstExpr::Await(await_expr) => {
281                self.visit_expr(&await_expr.value);
282            }
283            AstExpr::Yield(yield_expr) => {
284                if let Some(value) = &yield_expr.value {
285                    self.visit_expr(value);
286                }
287            }
288            AstExpr::YieldFrom(yieldfrom) => {
289                self.visit_expr(&yieldfrom.value);
290            }
291            AstExpr::Compare(compare) => {
292                self.visit_expr(&compare.left);
293                for comparator in &compare.comparators {
294                    self.visit_expr(comparator);
295                }
296            }
297            AstExpr::Lambda(lambda) => {
298                self.visit_expr(&lambda.body);
299            }
300            AstExpr::List(list) => {
301                for elt in &list.elts {
302                    self.visit_expr(elt);
303                }
304            }
305            AstExpr::Tuple(tuple) => {
306                for elt in &tuple.elts {
307                    self.visit_expr(elt);
308                }
309            }
310            AstExpr::Slice(slice) => {
311                if let Some(lower) = &slice.lower {
312                    self.visit_expr(lower);
313                }
314                if let Some(upper) = &slice.upper {
315                    self.visit_expr(upper);
316                }
317                if let Some(step) = &slice.step {
318                    self.visit_expr(step);
319                }
320            }
321            AstExpr::Subscript(subscript) => {
322                self.visit_expr(&subscript.value);
323                self.visit_expr(&subscript.slice);
324            }
325            AstExpr::Starred(starred) => {
326                self.visit_expr(&starred.value);
327            }
328            AstExpr::NamedExpr(named) => {
329                self.visit_expr(&named.target);
330                self.visit_expr(&named.value);
331            }
332            AstExpr::Name(_) | AstExpr::Constant(_) => {
333                // Leaf nodes, nothing to visit
334            }
335            _ => {
336                // Other expression types we don't handle yet
337            }
338        }
339    }
340
341    pub fn visit_stmt(&mut self, stmt: &'a AstStmt) {
342        match stmt {
343            AstStmt::FunctionDef(func) => {
344                // Visit function body
345                for stmt in &func.body {
346                    self.visit_stmt(stmt);
347                }
348            }
349            AstStmt::AsyncFunctionDef(func) => {
350                // Visit async function body
351                for stmt in &func.body {
352                    self.visit_stmt(stmt);
353                }
354            }
355            AstStmt::ClassDef(class) => {
356                // Visit class body
357                for stmt in &class.body {
358                    self.visit_stmt(stmt);
359                }
360            }
361            AstStmt::Return(ret) => {
362                if let Some(value) = &ret.value {
363                    self.visit_expr(value);
364                }
365            }
366            AstStmt::Delete(del) => {
367                for target in &del.targets {
368                    self.visit_expr(target);
369                }
370            }
371            AstStmt::Assign(assign) => {
372                self.visit_expr(&assign.value);
373                for target in &assign.targets {
374                    self.visit_expr(target);
375                }
376            }
377            AstStmt::AugAssign(augassign) => {
378                self.visit_expr(&augassign.target);
379                self.visit_expr(&augassign.value);
380            }
381            AstStmt::AnnAssign(annassign) => {
382                self.visit_expr(&annassign.target);
383                if let Some(value) = &annassign.value {
384                    self.visit_expr(value);
385                }
386            }
387            AstStmt::For(for_stmt) => {
388                self.visit_expr(&for_stmt.target);
389                self.visit_expr(&for_stmt.iter);
390                for stmt in &for_stmt.body {
391                    self.visit_stmt(stmt);
392                }
393                for stmt in &for_stmt.orelse {
394                    self.visit_stmt(stmt);
395                }
396            }
397            AstStmt::AsyncFor(for_stmt) => {
398                self.visit_expr(&for_stmt.target);
399                self.visit_expr(&for_stmt.iter);
400                for stmt in &for_stmt.body {
401                    self.visit_stmt(stmt);
402                }
403                for stmt in &for_stmt.orelse {
404                    self.visit_stmt(stmt);
405                }
406            }
407            AstStmt::While(while_stmt) => {
408                self.visit_expr(&while_stmt.test);
409                for stmt in &while_stmt.body {
410                    self.visit_stmt(stmt);
411                }
412                for stmt in &while_stmt.orelse {
413                    self.visit_stmt(stmt);
414                }
415            }
416            AstStmt::If(if_stmt) => {
417                self.visit_expr(&if_stmt.test);
418                for stmt in &if_stmt.body {
419                    self.visit_stmt(stmt);
420                }
421                for stmt in &if_stmt.orelse {
422                    self.visit_stmt(stmt);
423                }
424            }
425            AstStmt::With(with_stmt) => {
426                for item in &with_stmt.items {
427                    self.visit_expr(&item.context_expr);
428                }
429                for stmt in &with_stmt.body {
430                    self.visit_stmt(stmt);
431                }
432            }
433            AstStmt::AsyncWith(with_stmt) => {
434                for item in &with_stmt.items {
435                    self.visit_expr(&item.context_expr);
436                }
437                for stmt in &with_stmt.body {
438                    self.visit_stmt(stmt);
439                }
440            }
441            AstStmt::Raise(raise_stmt) => {
442                if let Some(exc) = &raise_stmt.exc {
443                    self.visit_expr(exc);
444                }
445                if let Some(cause) = &raise_stmt.cause {
446                    self.visit_expr(cause);
447                }
448            }
449            AstStmt::Try(try_stmt) => {
450                for stmt in &try_stmt.body {
451                    self.visit_stmt(stmt);
452                }
453                for handler in &try_stmt.handlers {
454                    match handler {
455                        AstExceptHandler::ExceptHandler(h) => {
456                            for stmt in &h.body {
457                                self.visit_stmt(stmt);
458                            }
459                        }
460                    }
461                }
462                for stmt in &try_stmt.orelse {
463                    self.visit_stmt(stmt);
464                }
465                for stmt in &try_stmt.finalbody {
466                    self.visit_stmt(stmt);
467                }
468            }
469            AstStmt::Assert(assert_stmt) => {
470                self.visit_expr(&assert_stmt.test);
471                if let Some(msg) = &assert_stmt.msg {
472                    self.visit_expr(msg);
473                }
474            }
475            AstStmt::Expr(expr_stmt) => {
476                self.visit_expr(&expr_stmt.value);
477            }
478            AstStmt::Pass(_) | AstStmt::Break(_) | AstStmt::Continue(_) => {
479                // Nothing to visit
480            }
481            _ => {
482                // Other statement types we don't handle yet
483            }
484        }
485    }
486
487    fn get_function_name(&self, expr: &AstExpr) -> Option<String> {
488        match expr {
489            AstExpr::Name(name) => {
490                let simple_name = name.id.to_string();
491
492                // First check if this name was imported
493                if let Some(full_name) = self.import_map.get(&simple_name) {
494                    // This name was imported, use its full module path
495                    if self.replacements_info.contains_key(full_name) {
496                        return Some(full_name.clone());
497                    }
498                }
499
500                // Check if we have a qualified version of this name
501                let qualified_name = format!("{}.{}", self.module_name, simple_name);
502                // Try the qualified name first, then the simple name
503                #[allow(clippy::map_entry)]
504                // This is not contains_key + insert, it's conditional lookup
505                if self.replacements_info.contains_key(&qualified_name) {
506                    Some(qualified_name)
507                } else if self.replacements_info.contains_key(&simple_name) {
508                    Some(simple_name)
509                } else {
510                    None
511                }
512            }
513            AstExpr::Attribute(attr) => {
514                // For method calls like obj.method(), we need to check all possible matches
515                let method_name = attr.attr.to_string();
516
517                // Try all keys that end with the method name
518                for key in self.replacements_info.keys() {
519                    if key.ends_with(&format!(".{}", method_name)) {
520                        return Some(key.clone());
521                    }
522                }
523
524                // Fallback to simple name
525                Some(method_name)
526            }
527            _ => None,
528        }
529    }
530
531    fn build_replacement(
532        &self,
533        replace_info: &ReplaceInfo,
534        call: &rustpython_ast::ExprCall,
535    ) -> String {
536        let mut replacement = replace_info.replacement_expr.clone();
537
538        // Strip module prefix if present
539        if replacement.starts_with(&format!("{}.", self.module_name)) {
540            replacement = replacement[self.module_name.len() + 1..].to_string();
541        }
542
543        // Special case: handle literal *args, **kwargs pattern
544        if replacement.contains("(*args, **kwargs)") {
545            // Build the actual call with all arguments
546            let func_name = replacement.split('(').next().unwrap_or(&replacement);
547            let mut new_call = format!("{}(", func_name);
548
549            // Add positional arguments
550            for (i, arg) in call.args.iter().enumerate() {
551                if i > 0 {
552                    new_call.push_str(", ");
553                }
554                new_call.push_str(&self.expr_to_source(arg));
555            }
556
557            // Add keyword arguments
558            if !call.keywords.is_empty() {
559                if !call.args.is_empty() {
560                    new_call.push_str(", ");
561                }
562                for (i, keyword) in call.keywords.iter().enumerate() {
563                    if i > 0 {
564                        new_call.push_str(", ");
565                    }
566                    if let Some(arg_name) = &keyword.arg {
567                        new_call.push_str(&format!(
568                            "{}={}",
569                            arg_name,
570                            self.expr_to_source(&keyword.value)
571                        ));
572                    } else {
573                        // **kwargs
574                        new_call.push_str(&format!("**{}", self.expr_to_source(&keyword.value)));
575                    }
576                }
577            }
578
579            new_call.push(')');
580            return new_call;
581        }
582
583        // Simple case: if no placeholders, just return the replacement as-is
584        if !replacement.contains("{") {
585            return replacement;
586        }
587
588        // Handle special cases like {self} placeholders
589        if replacement.contains("{self}") || replacement.contains("{cls}") {
590            // For method calls, we need to handle the receiver specially
591            if let AstExpr::Attribute(attr) = call.func.as_ref() {
592                let receiver = self.expr_to_source(&attr.value);
593                replacement = replacement.replace("{self}", &receiver);
594                replacement = replacement.replace("{cls}", &receiver);
595            }
596        }
597
598        // Build a map of parameter names to argument values
599        let mut arg_map = HashMap::new();
600
601        // Get the parameters, skipping self/cls if present
602        let params_start = if !replace_info.parameters.is_empty()
603            && (replace_info.parameters[0].name == "self"
604                || replace_info.parameters[0].name == "cls")
605        {
606            1
607        } else {
608            0
609        };
610
611        // Map positional arguments
612        for (i, arg) in call.args.iter().enumerate() {
613            if let Some(param) = replace_info.parameters.get(i + params_start) {
614                let arg_str = self.expr_to_source(arg);
615                arg_map.insert(param.name.clone(), arg_str);
616            }
617        }
618
619        // Map keyword arguments
620        for keyword in &call.keywords {
621            if let Some(arg_name) = &keyword.arg {
622                let arg_str = self.expr_to_source(&keyword.value);
623                arg_map.insert(arg_name.to_string(), arg_str);
624            } else {
625                // Handle **kwargs
626                let arg_str = self.expr_to_source(&keyword.value);
627                arg_map.insert("**kwargs".to_string(), format!("**{}", arg_str));
628            }
629        }
630
631        // Handle *args if present
632        let _has_varargs = replace_info.parameters.iter().any(|p| p.is_vararg);
633        let _has_kwargs = replace_info.parameters.iter().any(|p| p.is_kwarg);
634
635        // Replace placeholders with actual arguments
636        for (param_name, arg_value) in &arg_map {
637            let placeholder = format!("{{{}}}", param_name);
638            replacement = replacement.replace(&placeholder, arg_value);
639        }
640
641        // For *args and **kwargs, we need special handling
642        if replacement.contains("{*args}") {
643            // Calculate how many positional arguments are consumed by named parameters
644            let named_param_count = replace_info
645                .parameters
646                .iter()
647                .take_while(|p| !p.is_vararg && !p.is_kwarg && !p.is_kwonly)
648                .count();
649
650            // Skip parameters for self/cls if present
651            let actual_named_count = named_param_count.saturating_sub(params_start);
652
653            // Get the extra positional arguments that go to *args
654            let extra_args: Vec<String> = call
655                .args
656                .iter()
657                .skip(actual_named_count)
658                .map(|arg| self.expr_to_source(arg))
659                .collect();
660
661            let args_str = extra_args.join(", ");
662
663            // Replace {*args} with the extra arguments
664            if !args_str.is_empty() {
665                replacement = replacement.replace("{*args}", &args_str);
666            } else {
667                replacement = replacement.replace(", {*args}", "");
668                replacement = replacement.replace("{*args}", "");
669            }
670        }
671
672        if replacement.contains("{**kwargs}") {
673            // Collect all keyword arguments
674            let kwargs_strs: Vec<String> = call
675                .keywords
676                .iter()
677                .map(|kw| {
678                    if let Some(arg) = &kw.arg {
679                        format!("{}={}", arg, self.expr_to_source(&kw.value))
680                    } else {
681                        format!("**{}", self.expr_to_source(&kw.value))
682                    }
683                })
684                .collect();
685
686            if !kwargs_strs.is_empty() {
687                let kwargs_str = kwargs_strs.join(", ");
688                replacement = replacement.replace("{**kwargs}", &kwargs_str);
689            } else {
690                replacement = replacement.replace(", {**kwargs}", "");
691                replacement = replacement.replace("{**kwargs}", "");
692            }
693        }
694
695        // Clean up unresolved placeholders for parameters with default values
696        let mut cleaned = replacement.clone();
697
698        // Remove placeholders for parameters that have default values and weren't provided
699        // But only remove simple parameter assignments, not placeholders in complex expressions
700        for param in &replace_info.parameters {
701            if param.has_default && !arg_map.contains_key(&param.name) {
702                let placeholder = format!("{{{}}}", param.name);
703
704                // Only remove keyword argument patterns where the placeholder is the entire value
705                // Pattern: "param={param}" - remove entire assignment
706                cleaned = cleaned.replace(&format!(", {}={}", param.name, placeholder), "");
707                cleaned = cleaned.replace(&format!("{}={}, ", param.name, placeholder), "");
708                cleaned = cleaned.replace(&format!("{}={}", param.name, placeholder), "");
709
710                // Do NOT remove bare placeholders that might be part of complex expressions
711                // like "{cache_size} * 2" - these should remain as placeholders for later substitution
712            }
713        }
714
715        // Clean up any remaining syntax issues from placeholder removal
716        // Fix multiple consecutive commas
717        while cleaned.contains(", ,") {
718            cleaned = cleaned.replace(", ,", ",");
719        }
720        // Fix trailing commas before closing parenthesis
721        cleaned = cleaned.replace(", )", ")");
722        // Fix leading commas after opening parenthesis
723        cleaned = cleaned.replace("(, ", "(");
724
725        // Remove empty *args and **kwargs placeholders
726        cleaned = cleaned.replace(", {*args}", "");
727        cleaned = cleaned.replace("{*args}, ", "");
728        cleaned = cleaned.replace("({*args})", "()");
729        cleaned = cleaned.replace("{*args}", "");
730
731        cleaned = cleaned.replace(", {**kwargs}", "");
732        cleaned = cleaned.replace("{**kwargs}, ", "");
733        cleaned = cleaned.replace("({**kwargs})", "()");
734        cleaned = cleaned.replace("{**kwargs}", "");
735
736        cleaned
737    }
738
739    fn try_replace_magic_method_call(&self, call: &rustpython_ast::ExprCall) -> Option<String> {
740        // Check if this is a built-in function call that corresponds to a magic method
741        if let AstExpr::Name(name) = call.func.as_ref() {
742            let magic_method_map = match name.id.as_str() {
743                "int" => "__int__",
744                "repr" => "__repr__",
745                "bool" => "__bool__",
746                "len" => "__len__",
747                "str" => "__str__",
748                "float" => "__float__",
749                "bytes" => "__bytes__",
750                "hash" => "__hash__",
751                _ => return None,
752            };
753
754            // For now, we need exactly one argument (the object to call the method on)
755            if call.args.len() != 1 {
756                return None;
757            }
758
759            let obj_expr = &call.args[0];
760            let obj_str = self.expr_to_source(obj_expr);
761
762            // Look for a replacement info for this magic method
763            // We need to find if any of our replacements match this magic method pattern
764            for (key, replace_info) in &self.replacements_info {
765                // Check if this replacement is for a magic method that matches
766                if key.ends_with(&format!(".{}", magic_method_map)) {
767                    // Extract the class name from the key
768                    if let Some(class_name) = key.rsplit('.').nth(1) {
769                        // Basic heuristic: only apply magic method replacements if the argument
770                        // is a simple name or looks like it could be of the right type.
771                        // Avoid replacing complex expressions like obj.attr, obj[key], etc.
772                        if !self.should_apply_magic_method_replacement(obj_expr, class_name) {
773                            continue;
774                        }
775
776                        // Extract the replacement method name from the replacement expression
777                        // Handle patterns like:
778                        // 1. "{self}.method()" -> "obj.method()"
779                        // 2. "builtin({self}.method())" -> "obj.method()"
780                        let replacement_expr = &replace_info.replacement_expr;
781
782                        // First try to find pattern: "builtin({self}.method())" or "builtin({self}.attr)"
783                        // Handle both with and without module prefix
784                        let patterns = [
785                            format!("{}({{self}}.", name.id),                      // int({self}.
786                            format!("{}.{}({{self}}.", self.module_name, name.id), // test_module.int({self}.
787                        ];
788
789                        for builtin_start in &patterns {
790                            if replacement_expr.starts_with(builtin_start) {
791                                if let Some(inner_part) =
792                                    replacement_expr.strip_prefix(builtin_start)
793                                {
794                                    // Handle method calls: "builtin({self}.method())" -> "obj.method()"
795                                    if inner_part.ends_with("())") {
796                                        if let Some(method_name) = inner_part.strip_suffix("())") {
797                                            let result = format!("{}.{}()", obj_str, method_name);
798                                            return Some(result);
799                                        }
800                                    }
801                                    // Handle attributes: "builtin({self}.attr)" -> "obj.attr"
802                                    else if inner_part.ends_with(")") {
803                                        if let Some(attr_name) = inner_part.strip_suffix(")") {
804                                            let result = format!("{}.{}", obj_str, attr_name);
805                                            return Some(result);
806                                        }
807                                    }
808                                }
809                            }
810                        }
811
812                        // Fallback to direct patterns: "{self}.method()" or "self.method()"
813                        let method_expr = if replacement_expr.starts_with("{self}.") {
814                            replacement_expr.strip_prefix("{self}.")
815                        } else {
816                            replacement_expr.strip_prefix("self.")
817                        };
818
819                        if let Some(method_part) = method_expr {
820                            if let Some(method_name) = method_part.strip_suffix("()") {
821                                let result = format!("{}.{}()", obj_str, method_name);
822                                return Some(result);
823                            }
824                        }
825
826                        // Handle function calls with {self} as parameter: "func({self})" -> "func(obj)"
827                        if replacement_expr.contains("({self})") {
828                            let mut result =
829                                replacement_expr.replace("({self})", &format!("({})", obj_str));
830                            // Strip module prefix if present
831                            if result.starts_with(&format!("{}.", self.module_name)) {
832                                result = result[self.module_name.len() + 1..].to_string();
833                            }
834                            return Some(result);
835                        }
836
837                        // Handle function calls with self as parameter: "func(self)" -> "func(obj)"
838                        if replacement_expr.contains("(self)") {
839                            let result =
840                                replacement_expr.replace("(self)", &format!("({})", obj_str));
841                            return Some(result);
842                        }
843
844                        // Handle simple expressions like "True", "False", constants, etc.
845                        // If the replacement doesn't contain placeholders or method calls, use it directly
846                        if !replacement_expr.contains("{") && !replacement_expr.contains("(") {
847                            // This is a simple expression (constant, variable name, etc.)
848                            return Some(replacement_expr.clone());
849                        }
850                    }
851                }
852            }
853        }
854
855        None
856    }
857
858    fn should_apply_magic_method_replacement(&self, obj_expr: &AstExpr, _class_name: &str) -> bool {
859        // Basic heuristic to determine if we should apply a magic method replacement
860        match obj_expr {
861            // Simple names like 'obj' are likely candidates for replacement
862            AstExpr::Name(_) => true,
863
864            // Complex expressions like obj.attr, obj[key], func() should NOT be replaced
865            // since they're likely to be of different types
866            AstExpr::Attribute(_) => false,
867            AstExpr::Subscript(_) => false,
868            AstExpr::Call(_) => false,
869
870            // TODO: We could implement more sophisticated type checking here
871            // using the type introspection context to actually determine the type
872            // of the expression and check if it matches class_name
873
874            // For now, be conservative and only replace simple names
875            _ => false,
876        }
877    }
878
879    fn expr_to_source(&self, expr: &AstExpr) -> String {
880        // Always reconstruct from AST to ensure proper escaping and formatting
881        self.reconstruct_expr(expr)
882    }
883
884    #[allow(clippy::only_used_in_recursion)] // self is needed for recursive method calls
885    fn reconstruct_expr(&self, expr: &AstExpr) -> String {
886        match expr {
887            AstExpr::Constant(c) => match &c.value {
888                rustpython_ast::Constant::Str(s) => {
889                    // Use proper string literal escaping
890                    format!(
891                        "\"{}\"",
892                        s.chars()
893                            .map(|c| match c {
894                                '"' => "\\\"".to_string(),
895                                '\\' => "\\\\".to_string(),
896                                '\n' => "\\n".to_string(),
897                                '\r' => "\\r".to_string(),
898                                '\t' => "\\t".to_string(),
899                                c if c.is_control() => format!("\\x{:02x}", c as u8),
900                                c => c.to_string(),
901                            })
902                            .collect::<String>()
903                    )
904                }
905                rustpython_ast::Constant::Int(i) => i.to_string(),
906                rustpython_ast::Constant::Float(f) => f.to_string(),
907                rustpython_ast::Constant::Bool(b) => {
908                    if *b {
909                        "True".to_string()
910                    } else {
911                        "False".to_string()
912                    }
913                }
914                rustpython_ast::Constant::None => "None".to_string(),
915                rustpython_ast::Constant::Bytes(b) => {
916                    // Properly escape bytes literal
917                    let escaped = b
918                        .iter()
919                        .map(|&byte| match byte {
920                            b'"' => "\\\"".to_string(),
921                            b'\\' => "\\\\".to_string(),
922                            b'\n' => "\\n".to_string(),
923                            b'\r' => "\\r".to_string(),
924                            b'\t' => "\\t".to_string(),
925                            b'\0' => "\\x00".to_string(),
926                            b if b.is_ascii_graphic() || b == b' ' => (b as char).to_string(),
927                            b => format!("\\x{:02x}", b),
928                        })
929                        .collect::<String>();
930                    format!("b\"{}\"", escaped)
931                }
932                rustpython_ast::Constant::Complex { real, imag } => {
933                    if *real == 0.0 {
934                        format!("{}j", imag)
935                    } else if *imag >= 0.0 {
936                        format!("{} + {}j", real, imag)
937                    } else {
938                        format!("{} - {}j", real, -imag)
939                    }
940                }
941                rustpython_ast::Constant::Ellipsis => "...".to_string(),
942                _ => "...".to_string(),
943            },
944            AstExpr::Name(name) => name.id.to_string(),
945            AstExpr::Attribute(attr) => {
946                format!("{}.{}", self.reconstruct_expr(&attr.value), attr.attr)
947            }
948            AstExpr::Subscript(subscript) => {
949                format!(
950                    "{}[{}]",
951                    self.reconstruct_expr(&subscript.value),
952                    self.reconstruct_expr(&subscript.slice)
953                )
954            }
955            AstExpr::Slice(slice) => {
956                let lower = slice
957                    .lower
958                    .as_ref()
959                    .map(|e| self.reconstruct_expr(e))
960                    .unwrap_or_default();
961                let upper = slice
962                    .upper
963                    .as_ref()
964                    .map(|e| self.reconstruct_expr(e))
965                    .unwrap_or_default();
966                let step = slice
967                    .step
968                    .as_ref()
969                    .map(|e| format!(":{}", self.reconstruct_expr(e)))
970                    .unwrap_or_default();
971                format!("{}:{}{}", lower, upper, step)
972            }
973            AstExpr::BinOp(binop) => {
974                let left = self.reconstruct_expr(&binop.left);
975                let right = self.reconstruct_expr(&binop.right);
976                let op = match binop.op {
977                    rustpython_ast::Operator::Add => "+",
978                    rustpython_ast::Operator::Sub => "-",
979                    rustpython_ast::Operator::Mult => "*",
980                    rustpython_ast::Operator::MatMult => "@",
981                    rustpython_ast::Operator::Div => "/",
982                    rustpython_ast::Operator::Mod => "%",
983                    rustpython_ast::Operator::Pow => "**",
984                    rustpython_ast::Operator::LShift => "<<",
985                    rustpython_ast::Operator::RShift => ">>",
986                    rustpython_ast::Operator::BitOr => "|",
987                    rustpython_ast::Operator::BitXor => "^",
988                    rustpython_ast::Operator::BitAnd => "&",
989                    rustpython_ast::Operator::FloorDiv => "//",
990                };
991                format!("{} {} {}", left, op, right)
992            }
993            AstExpr::UnaryOp(unaryop) => {
994                let operand = self.reconstruct_expr(&unaryop.operand);
995                let op = match unaryop.op {
996                    rustpython_ast::UnaryOp::Invert => "~",
997                    rustpython_ast::UnaryOp::Not => "not ",
998                    rustpython_ast::UnaryOp::UAdd => "+",
999                    rustpython_ast::UnaryOp::USub => "-",
1000                };
1001                format!("{}{}", op, operand)
1002            }
1003            AstExpr::BoolOp(boolop) => {
1004                let op = match boolop.op {
1005                    rustpython_ast::BoolOp::And => " and ",
1006                    rustpython_ast::BoolOp::Or => " or ",
1007                };
1008                boolop
1009                    .values
1010                    .iter()
1011                    .map(|v| self.reconstruct_expr(v))
1012                    .collect::<Vec<_>>()
1013                    .join(op)
1014            }
1015            AstExpr::Compare(compare) => {
1016                let mut result = self.reconstruct_expr(&compare.left);
1017                for (op, comparator) in compare.ops.iter().zip(&compare.comparators) {
1018                    let op_str = match op {
1019                        rustpython_ast::CmpOp::Eq => " == ",
1020                        rustpython_ast::CmpOp::NotEq => " != ",
1021                        rustpython_ast::CmpOp::Lt => " < ",
1022                        rustpython_ast::CmpOp::LtE => " <= ",
1023                        rustpython_ast::CmpOp::Gt => " > ",
1024                        rustpython_ast::CmpOp::GtE => " >= ",
1025                        rustpython_ast::CmpOp::Is => " is ",
1026                        rustpython_ast::CmpOp::IsNot => " is not ",
1027                        rustpython_ast::CmpOp::In => " in ",
1028                        rustpython_ast::CmpOp::NotIn => " not in ",
1029                    };
1030                    result.push_str(op_str);
1031                    result.push_str(&self.reconstruct_expr(comparator));
1032                }
1033                result
1034            }
1035            AstExpr::Call(call) => {
1036                let func = self.reconstruct_expr(&call.func);
1037                // Combine positional and keyword arguments using iterators
1038                let args: Vec<String> = call
1039                    .args
1040                    .iter()
1041                    .map(|arg| self.reconstruct_expr(arg))
1042                    .chain(call.keywords.iter().map(|keyword| {
1043                        if let Some(arg_name) = &keyword.arg {
1044                            format!("{}={}", arg_name, self.reconstruct_expr(&keyword.value))
1045                        } else {
1046                            format!("**{}", self.reconstruct_expr(&keyword.value))
1047                        }
1048                    }))
1049                    .collect();
1050
1051                format!("{}({})", func, args.join(", "))
1052            }
1053            AstExpr::List(list) => {
1054                let elements: Vec<String> =
1055                    list.elts.iter().map(|e| self.reconstruct_expr(e)).collect();
1056                format!("[{}]", elements.join(", "))
1057            }
1058            AstExpr::Tuple(tuple) => {
1059                let elements: Vec<String> = tuple
1060                    .elts
1061                    .iter()
1062                    .map(|e| self.reconstruct_expr(e))
1063                    .collect();
1064                if elements.len() == 1 {
1065                    format!("({},)", elements[0])
1066                } else {
1067                    format!("({})", elements.join(", "))
1068                }
1069            }
1070            AstExpr::Dict(dict) => {
1071                let items: Vec<String> = dict
1072                    .keys
1073                    .iter()
1074                    .zip(&dict.values)
1075                    .map(|(key, value)| {
1076                        if let Some(k) = key {
1077                            format!(
1078                                "{}: {}",
1079                                self.reconstruct_expr(k),
1080                                self.reconstruct_expr(value)
1081                            )
1082                        } else {
1083                            format!("**{}", self.reconstruct_expr(value))
1084                        }
1085                    })
1086                    .collect();
1087                format!("{{{}}}", items.join(", "))
1088            }
1089            AstExpr::Set(set) => {
1090                let elements: Vec<String> =
1091                    set.elts.iter().map(|e| self.reconstruct_expr(e)).collect();
1092                if elements.is_empty() {
1093                    "set()".to_string()
1094                } else {
1095                    format!("{{{}}}", elements.join(", "))
1096                }
1097            }
1098            AstExpr::ListComp(comp) => {
1099                let elt = self.reconstruct_expr(&comp.elt);
1100
1101                // Handle all generators (for multiple 'for' clauses)
1102                let mut generators = Vec::new();
1103                for gen in &comp.generators {
1104                    let target = self.reconstruct_expr(&gen.target);
1105                    let iter = self.reconstruct_expr(&gen.iter);
1106                    let mut gen_str = format!("for {} in {}", target, iter);
1107
1108                    // Add conditions for this generator
1109                    if !gen.ifs.is_empty() {
1110                        let conds: Vec<String> = gen
1111                            .ifs
1112                            .iter()
1113                            .map(|if_expr| self.reconstruct_expr(if_expr))
1114                            .collect();
1115                        gen_str.push_str(&format!(" if {}", conds.join(" if ")));
1116                    }
1117
1118                    generators.push(gen_str);
1119                }
1120
1121                format!("[{} {}]", elt, generators.join(" "))
1122            }
1123            AstExpr::SetComp(comp) => {
1124                let conditions = if comp.generators[0].ifs.is_empty() {
1125                    String::new()
1126                } else {
1127                    let conds: Vec<String> = comp.generators[0]
1128                        .ifs
1129                        .iter()
1130                        .map(|if_expr| self.reconstruct_expr(if_expr))
1131                        .collect();
1132                    format!(" if {}", conds.join(" if "))
1133                };
1134                format!(
1135                    "{{{} for {} in {}{}}}",
1136                    self.reconstruct_expr(&comp.elt),
1137                    self.reconstruct_expr(&comp.generators[0].target),
1138                    self.reconstruct_expr(&comp.generators[0].iter),
1139                    conditions
1140                )
1141            }
1142            AstExpr::DictComp(comp) => {
1143                let conditions = if comp.generators[0].ifs.is_empty() {
1144                    String::new()
1145                } else {
1146                    let conds: Vec<String> = comp.generators[0]
1147                        .ifs
1148                        .iter()
1149                        .map(|if_expr| self.reconstruct_expr(if_expr))
1150                        .collect();
1151                    format!(" if {}", conds.join(" if "))
1152                };
1153                format!(
1154                    "{{{}: {} for {} in {}{}}}",
1155                    self.reconstruct_expr(&comp.key),
1156                    self.reconstruct_expr(&comp.value),
1157                    self.reconstruct_expr(&comp.generators[0].target),
1158                    self.reconstruct_expr(&comp.generators[0].iter),
1159                    conditions
1160                )
1161            }
1162            AstExpr::GeneratorExp(gen) => {
1163                let conditions = if gen.generators[0].ifs.is_empty() {
1164                    String::new()
1165                } else {
1166                    let conds: Vec<String> = gen.generators[0]
1167                        .ifs
1168                        .iter()
1169                        .map(|if_expr| self.reconstruct_expr(if_expr))
1170                        .collect();
1171                    format!(" if {}", conds.join(" if "))
1172                };
1173                format!(
1174                    "({} for {} in {}{})",
1175                    self.reconstruct_expr(&gen.elt),
1176                    self.reconstruct_expr(&gen.generators[0].target),
1177                    self.reconstruct_expr(&gen.generators[0].iter),
1178                    conditions
1179                )
1180            }
1181            AstExpr::IfExp(ifexp) => {
1182                format!(
1183                    "{} if {} else {}",
1184                    self.reconstruct_expr(&ifexp.body),
1185                    self.reconstruct_expr(&ifexp.test),
1186                    self.reconstruct_expr(&ifexp.orelse)
1187                )
1188            }
1189            AstExpr::Lambda(lambda) => {
1190                let args: Vec<String> = lambda
1191                    .args
1192                    .args
1193                    .iter()
1194                    .map(|arg| arg.def.arg.to_string())
1195                    .collect();
1196                format!(
1197                    "lambda {}: {}",
1198                    args.join(", "),
1199                    self.reconstruct_expr(&lambda.body)
1200                )
1201            }
1202            AstExpr::Starred(starred) => {
1203                format!("*{}", self.reconstruct_expr(&starred.value))
1204            }
1205            AstExpr::Yield(yield_expr) => {
1206                if let Some(value) = &yield_expr.value {
1207                    format!("yield {}", self.reconstruct_expr(value))
1208                } else {
1209                    "yield".to_string()
1210                }
1211            }
1212            AstExpr::YieldFrom(yieldfrom) => {
1213                format!("yield from {}", self.reconstruct_expr(&yieldfrom.value))
1214            }
1215            AstExpr::Await(await_expr) => {
1216                format!("await {}", self.reconstruct_expr(&await_expr.value))
1217            }
1218            AstExpr::JoinedStr(joined) => {
1219                let mut result = String::from("f\"");
1220                for value in &joined.values {
1221                    match value {
1222                        AstExpr::Constant(c) => {
1223                            if let rustpython_ast::Constant::Str(s) = &c.value {
1224                                result.push_str(s);
1225                            }
1226                        }
1227                        AstExpr::FormattedValue(fmt) => {
1228                            result.push('{');
1229                            result.push_str(&self.reconstruct_expr(&fmt.value));
1230                            match fmt.conversion {
1231                                rustpython_ast::ConversionFlag::Str => result.push_str("!s"),
1232                                rustpython_ast::ConversionFlag::Repr => result.push_str("!r"),
1233                                rustpython_ast::ConversionFlag::Ascii => result.push_str("!a"),
1234                                rustpython_ast::ConversionFlag::None => {}
1235                            }
1236                            if let Some(spec) = &fmt.format_spec {
1237                                result.push(':');
1238                                match &**spec {
1239                                    AstExpr::Constant(c) => {
1240                                        if let rustpython_ast::Constant::Str(s) = &c.value {
1241                                            result.push_str(s);
1242                                        }
1243                                    }
1244                                    AstExpr::JoinedStr(_) => {
1245                                        // For f-strings inside format specs, we need special handling
1246                                        // This shouldn't normally happen in valid Python
1247                                        let reconstructed = self.reconstruct_expr(spec);
1248                                        // Remove the f" prefix and " suffix if it's an f-string
1249                                        if reconstructed.starts_with("f\"")
1250                                            && reconstructed.ends_with('"')
1251                                        {
1252                                            result.push_str(
1253                                                &reconstructed[2..reconstructed.len() - 1],
1254                                            );
1255                                        } else {
1256                                            result.push_str(&reconstructed);
1257                                        }
1258                                    }
1259                                    _ => result.push_str(&self.reconstruct_expr(spec)),
1260                                }
1261                            }
1262                            result.push('}');
1263                        }
1264                        _ => {
1265                            result.push('{');
1266                            result.push_str(&self.reconstruct_expr(value));
1267                            result.push('}');
1268                        }
1269                    }
1270                }
1271                result.push('"');
1272                result
1273            }
1274            AstExpr::FormattedValue(fmt) => {
1275                // This should normally be handled within JoinedStr
1276                let mut result = String::new();
1277                result.push('{');
1278                result.push_str(&self.reconstruct_expr(&fmt.value));
1279                match fmt.conversion {
1280                    rustpython_ast::ConversionFlag::Str => result.push_str("!s"),
1281                    rustpython_ast::ConversionFlag::Repr => result.push_str("!r"),
1282                    rustpython_ast::ConversionFlag::Ascii => result.push_str("!a"),
1283                    rustpython_ast::ConversionFlag::None => {}
1284                }
1285                if let Some(spec) = &fmt.format_spec {
1286                    result.push(':');
1287                    match &**spec {
1288                        AstExpr::Constant(c) => {
1289                            if let rustpython_ast::Constant::Str(s) = &c.value {
1290                                result.push_str(s);
1291                            }
1292                        }
1293                        _ => result.push_str(&self.reconstruct_expr(spec)),
1294                    }
1295                }
1296                result.push('}');
1297                result
1298            }
1299            AstExpr::NamedExpr(named) => {
1300                format!(
1301                    "{} := {}",
1302                    self.reconstruct_expr(&named.target),
1303                    self.reconstruct_expr(&named.value)
1304                )
1305            }
1306        }
1307    }
1308}
1309
1310/// Apply replacements to source code preserving formatting
1311pub fn apply_replacements(source: &str, mut replacements: Vec<(Range<usize>, String)>) -> String {
1312    // Sort replacements by start position (reverse order for applying)
1313    replacements.sort_by_key(|(range, _)| std::cmp::Reverse(range.start));
1314
1315    let mut result = source.to_string();
1316
1317    for (range, replacement) in replacements {
1318        let original_text = &source[range.clone()];
1319        tracing::debug!(
1320            "Applying replacement at {:?}: '{}' -> '{}'",
1321            range,
1322            original_text,
1323            replacement
1324        );
1325        result.replace_range(range, &replacement);
1326    }
1327
1328    result
1329}
1330
1331/// Main entry point for migrating a file using Ruff parser
1332pub fn migrate_file_with_ruff(
1333    source: &str,
1334    module_name: &str,
1335    file_path: String,
1336    type_introspection: TypeIntrospectionMethod,
1337) -> Result<String> {
1338    // Use the main migrate_file function from migrate_stub
1339    use crate::type_introspection_context::TypeIntrospectionContext;
1340    use std::path::Path;
1341
1342    let mut type_context = TypeIntrospectionContext::new(type_introspection)?;
1343
1344    let result = crate::migrate_stub::migrate_file(
1345        source,
1346        module_name,
1347        Path::new(&file_path),
1348        &mut type_context,
1349        std::collections::HashMap::new(), // No predefined replacements
1350        std::collections::HashMap::new(), // No dependency inheritance map
1351    );
1352
1353    type_context.shutdown()?;
1354    result
1355}
1356
1357/// Compatibility function for import tracking tests
1358/// This provides the same interface as the old ruff_parser_improved module
1359pub fn migrate_file_with_improved_ruff(
1360    source: &str,
1361    module_name: &str,
1362    file_path: String,
1363    type_introspection: TypeIntrospectionMethod,
1364) -> Result<String> {
1365    // For now, delegate to the standard migration function
1366    // In the future, this could include additional import tracking functionality
1367    migrate_file_with_ruff(source, module_name, file_path, type_introspection)
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372    use super::*;
1373
1374    #[test]
1375    fn test_parse_simple() {
1376        let source = "x = 1\ny = 2";
1377        let module = PythonModule::parse(source).unwrap();
1378        assert_eq!(module.ast().len(), 2);
1379    }
1380
1381    #[test]
1382    fn test_position_mapping() {
1383        let source = "x = 1\ny = 2";
1384        let module = PythonModule::parse(source).unwrap();
1385
1386        // First line, first column
1387        assert_eq!(module.offset_to_position(0), Some((1, 0)));
1388
1389        // Second line start
1390        assert_eq!(module.offset_to_position(6), Some((2, 0)));
1391    }
1392}