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