1use std::collections::HashSet;
9
10use tree_sitter::{Node, Tree};
11
12use crate::indent::IndentStyle;
13use crate::parser::{grammar_for, node_text, LangId};
14
15#[derive(Debug)]
21pub struct FreeVariableResult {
22 pub parameters: Vec<String>,
25 pub has_this_or_self: bool,
27}
28
29pub fn detect_free_variables(
39 source: &str,
40 tree: &Tree,
41 start_byte: usize,
42 end_byte: usize,
43 lang: LangId,
44) -> FreeVariableResult {
45 let root = tree.root_node();
46
47 let mut references: Vec<String> = Vec::new();
49 collect_identifier_refs(&root, source, start_byte, end_byte, lang, &mut references);
50
51 let mut local_decls: HashSet<String> = HashSet::new();
53 collect_declarations_in_range(&root, source, start_byte, end_byte, lang, &mut local_decls);
54
55 let enclosing_fn = find_enclosing_function(&root, start_byte, lang);
57
58 let mut enclosing_decls: HashSet<String> = HashSet::new();
60 if let Some(fn_node) = enclosing_fn {
61 collect_declarations_in_range(
62 &fn_node,
63 source,
64 fn_node.start_byte(),
65 start_byte, lang,
67 &mut enclosing_decls,
68 );
69 collect_function_params(&fn_node, source, lang, &mut enclosing_decls);
71 }
72
73 let has_this_or_self = check_this_or_self(&root, source, start_byte, end_byte, lang);
75
76 let mut seen = HashSet::new();
79 let mut parameters = Vec::new();
80 for name in &references {
81 if local_decls.contains(name) {
82 continue;
83 }
84 if !seen.insert(name.clone()) {
85 continue; }
87 if enclosing_decls.contains(name) {
88 parameters.push(name.clone());
89 }
90 }
92
93 FreeVariableResult {
94 parameters,
95 has_this_or_self,
96 }
97}
98
99fn collect_identifier_refs(
102 node: &Node,
103 source: &str,
104 start_byte: usize,
105 end_byte: usize,
106 lang: LangId,
107 out: &mut Vec<String>,
108) {
109 if node.end_byte() <= start_byte || node.start_byte() >= end_byte {
111 return;
112 }
113
114 let kind = node.kind();
115
116 if kind == "identifier" && node.start_byte() >= start_byte && node.end_byte() <= end_byte {
118 if !is_property_access(node, lang) {
121 let name = node_text(source, node).to_string();
122 if !is_keyword(&name, lang) {
124 out.push(name);
125 }
126 }
127 }
128
129 let mut cursor = node.walk();
131 if cursor.goto_first_child() {
132 loop {
133 collect_identifier_refs(&cursor.node(), source, start_byte, end_byte, lang, out);
134 if !cursor.goto_next_sibling() {
135 break;
136 }
137 }
138 }
139}
140
141fn is_property_access(node: &Node, lang: LangId) -> bool {
143 if let Some(parent) = node.parent() {
147 let pk = parent.kind();
148 match lang {
149 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
150 if pk == "member_expression" {
152 if let Some(prop) = parent.child_by_field_name("property") {
153 return prop.id() == node.id();
154 }
155 }
156 }
157 LangId::Python => {
158 if pk == "attribute" {
160 if let Some(attr) = parent.child_by_field_name("attribute") {
161 return attr.id() == node.id();
162 }
163 }
164 }
165 _ => {}
166 }
167 }
168 false
169}
170
171fn is_keyword(name: &str, lang: LangId) -> bool {
173 match lang {
174 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => matches!(
175 name,
176 "undefined" | "null" | "true" | "false" | "NaN" | "Infinity" | "console" | "require"
177 ),
178 LangId::Python => matches!(
179 name,
180 "None"
181 | "True"
182 | "False"
183 | "print"
184 | "len"
185 | "range"
186 | "str"
187 | "int"
188 | "float"
189 | "list"
190 | "dict"
191 | "set"
192 | "tuple"
193 | "type"
194 | "super"
195 | "isinstance"
196 | "enumerate"
197 | "zip"
198 | "map"
199 | "filter"
200 | "sorted"
201 | "reversed"
202 | "any"
203 | "all"
204 | "min"
205 | "max"
206 | "sum"
207 | "abs"
208 | "open"
209 | "input"
210 | "format"
211 | "hasattr"
212 | "getattr"
213 | "setattr"
214 | "delattr"
215 | "repr"
216 | "iter"
217 | "next"
218 | "ValueError"
219 | "TypeError"
220 | "KeyError"
221 | "IndexError"
222 | "Exception"
223 | "RuntimeError"
224 | "StopIteration"
225 | "NotImplementedError"
226 | "AttributeError"
227 | "ImportError"
228 | "OSError"
229 | "IOError"
230 | "FileNotFoundError"
231 ),
232 _ => false,
233 }
234}
235
236fn collect_declarations_in_range(
238 node: &Node,
239 source: &str,
240 start_byte: usize,
241 end_byte: usize,
242 lang: LangId,
243 out: &mut HashSet<String>,
244) {
245 if node.end_byte() <= start_byte || node.start_byte() >= end_byte {
246 return;
247 }
248
249 let kind = node.kind();
250
251 match lang {
252 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
253 if kind == "variable_declarator" {
255 if let Some(name_node) = node.child_by_field_name("name") {
256 if name_node.start_byte() >= start_byte && name_node.end_byte() <= end_byte {
257 out.insert(node_text(source, &name_node).to_string());
258 }
259 }
260 }
261 }
262 LangId::Python => {
263 if kind == "assignment" {
265 if let Some(left) = node.child_by_field_name("left") {
266 if left.kind() == "identifier"
267 && left.start_byte() >= start_byte
268 && left.end_byte() <= end_byte
269 {
270 out.insert(node_text(source, &left).to_string());
271 }
272 }
273 }
274 }
275 _ => {}
276 }
277
278 let mut cursor = node.walk();
280 if cursor.goto_first_child() {
281 loop {
282 collect_declarations_in_range(&cursor.node(), source, start_byte, end_byte, lang, out);
283 if !cursor.goto_next_sibling() {
284 break;
285 }
286 }
287 }
288}
289
290fn collect_function_params(fn_node: &Node, source: &str, lang: LangId, out: &mut HashSet<String>) {
292 match lang {
293 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
294 if let Some(params) = fn_node.child_by_field_name("parameters") {
296 collect_param_identifiers(¶ms, source, lang, out);
297 }
298 let mut cursor = fn_node.walk();
300 if cursor.goto_first_child() {
301 loop {
302 let child = cursor.node();
303 if child.kind() == "variable_declarator" {
304 if let Some(value) = child.child_by_field_name("value") {
305 if value.kind() == "arrow_function" {
306 if let Some(params) = value.child_by_field_name("parameters") {
307 collect_param_identifiers(¶ms, source, lang, out);
308 }
309 }
310 }
311 }
312 if !cursor.goto_next_sibling() {
313 break;
314 }
315 }
316 }
317 }
318 LangId::Python => {
319 if let Some(params) = fn_node.child_by_field_name("parameters") {
320 collect_param_identifiers(¶ms, source, lang, out);
321 }
322 }
323 _ => {}
324 }
325}
326
327fn collect_param_identifiers(
329 params_node: &Node,
330 source: &str,
331 lang: LangId,
332 out: &mut HashSet<String>,
333) {
334 let mut cursor = params_node.walk();
335 if cursor.goto_first_child() {
336 loop {
337 let child = cursor.node();
338 match lang {
339 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
340 if child.kind() == "required_parameter" || child.kind() == "optional_parameter"
343 {
344 if let Some(pattern) = child.child_by_field_name("pattern") {
345 if pattern.kind() == "identifier" {
346 out.insert(node_text(source, &pattern).to_string());
347 }
348 }
349 } else if child.kind() == "identifier" {
350 out.insert(node_text(source, &child).to_string());
351 }
352 }
353 LangId::Python => {
354 if child.kind() == "identifier" {
355 let name = node_text(source, &child).to_string();
356 if name != "self" {
358 out.insert(name);
359 }
360 }
361 }
362 _ => {}
363 }
364 if !cursor.goto_next_sibling() {
365 break;
366 }
367 }
368 }
369}
370
371fn find_enclosing_function<'a>(root: &Node<'a>, byte_pos: usize, lang: LangId) -> Option<Node<'a>> {
373 find_deepest_function_ancestor(root, byte_pos, lang)
374}
375
376fn find_deepest_function_ancestor<'a>(
378 node: &Node<'a>,
379 byte_pos: usize,
380 lang: LangId,
381) -> Option<Node<'a>> {
382 let mut result: Option<Node<'a>> = None;
383 if is_function_like_boundary(node, byte_pos, lang)
384 && node.start_byte() <= byte_pos
385 && byte_pos < node.end_byte()
386 {
387 result = Some(*node);
388 }
389
390 let child_count = node.child_count();
391 for i in 0..child_count {
392 if let Some(child) = node.child(i as u32) {
393 if child.start_byte() <= byte_pos && byte_pos < child.end_byte() {
394 if let Some(deeper) = find_deepest_function_ancestor(&child, byte_pos, lang) {
395 result = Some(deeper);
396 }
397 }
398 }
399 }
400
401 result
402}
403
404fn is_function_like_boundary(node: &Node, byte_pos: usize, lang: LangId) -> bool {
405 match lang {
406 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => match node.kind() {
407 "function_declaration"
408 | "method_definition"
409 | "arrow_function"
410 | "function_expression" => true,
411 "lexical_declaration" => lexical_declaration_has_function_initializer(node, byte_pos),
412 _ => false,
413 },
414 LangId::Python => node.kind() == "function_definition",
415 _ => false,
416 }
417}
418
419fn lexical_declaration_has_function_initializer(node: &Node, byte_pos: usize) -> bool {
420 let mut cursor = node.walk();
421 if cursor.goto_first_child() {
422 loop {
423 let child = cursor.node();
424 if child.kind() == "variable_declarator" {
425 if let Some(value) = child.child_by_field_name("value") {
426 if matches!(value.kind(), "arrow_function" | "function_expression")
427 && child.start_byte() <= byte_pos
428 && byte_pos < child.end_byte()
429 {
430 return true;
431 }
432 }
433 }
434 if !cursor.goto_next_sibling() {
435 break;
436 }
437 }
438 }
439
440 false
441}
442
443fn check_this_or_self(
445 node: &Node,
446 source: &str,
447 start_byte: usize,
448 end_byte: usize,
449 lang: LangId,
450) -> bool {
451 if node.end_byte() <= start_byte || node.start_byte() >= end_byte {
452 return false;
453 }
454
455 if node.start_byte() >= start_byte && node.end_byte() <= end_byte {
456 let kind = node.kind();
457 match lang {
458 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
459 if kind == "this" {
460 return true;
461 }
462 }
463 LangId::Python => {
464 if kind == "identifier" && node_text(source, node) == "self" {
465 if let Some(parent) = node.parent() {
467 if parent.kind() == "parameters" {
468 return false;
469 }
470 }
471 return true;
472 }
473 }
474 _ => {}
475 }
476 }
477
478 let mut cursor = node.walk();
479 if cursor.goto_first_child() {
480 loop {
481 if check_this_or_self(&cursor.node(), source, start_byte, end_byte, lang) {
482 return true;
483 }
484 if !cursor.goto_next_sibling() {
485 break;
486 }
487 }
488 }
489
490 false
491}
492
493#[derive(Debug, Clone, PartialEq, Eq)]
499pub enum ReturnKind {
500 Expression(String),
502 Variable(String),
504 Variables(Vec<String>),
506 Void,
508}
509
510const RETURN_VARIABLE_ASSIGNMENT_PREFIX: &str = "\0assignment:";
511
512#[derive(Debug, Clone, Copy, PartialEq, Eq)]
513enum JsDeclarationKind {
514 Const,
515 Let,
516 Var,
517 Assignment,
518}
519
520impl JsDeclarationKind {
521 fn label(self) -> &'static str {
522 match self {
523 Self::Const => "const",
524 Self::Let => "let",
525 Self::Var => "var",
526 Self::Assignment => "assignment",
527 }
528 }
529
530 fn multi_value_group(self) -> u8 {
531 match self {
532 Self::Const => 0,
533 Self::Let | Self::Var => 1,
534 Self::Assignment => 2,
535 }
536 }
537}
538
539#[derive(Debug, Clone, PartialEq, Eq)]
540struct ReturnVariableBinding {
541 name: String,
542 bound_names: Vec<String>,
543 js_kind: JsDeclarationKind,
544 representable: bool,
545}
546
547impl ReturnVariableBinding {
548 fn encoded_for_return_kind(&self) -> String {
549 match self.js_kind {
550 JsDeclarationKind::Const => self.name.clone(),
551 JsDeclarationKind::Let => format!("let {}", self.name),
552 JsDeclarationKind::Var => format!("var {}", self.name),
553 JsDeclarationKind::Assignment => {
554 format!("{}{}", RETURN_VARIABLE_ASSIGNMENT_PREFIX, self.name)
555 }
556 }
557 }
558}
559
560fn parse_return_variable(var: &str) -> ReturnVariableBinding {
561 if let Some(name) = var.strip_prefix(RETURN_VARIABLE_ASSIGNMENT_PREFIX) {
562 return ReturnVariableBinding {
563 name: name.to_string(),
564 bound_names: vec![name.to_string()],
565 js_kind: JsDeclarationKind::Assignment,
566 representable: true,
567 };
568 }
569
570 for (prefix, js_kind) in [
571 ("let ", JsDeclarationKind::Let),
572 ("var ", JsDeclarationKind::Var),
573 ("const ", JsDeclarationKind::Const),
574 ] {
575 if let Some(name) = var.strip_prefix(prefix) {
576 return ReturnVariableBinding {
577 name: name.to_string(),
578 bound_names: vec![name.to_string()],
579 js_kind,
580 representable: true,
581 };
582 }
583 }
584
585 ReturnVariableBinding {
586 name: var.to_string(),
587 bound_names: vec![var.to_string()],
588 js_kind: JsDeclarationKind::Const,
589 representable: true,
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Eq)]
594pub struct ReturnValueError {
595 pub message: String,
596}
597
598pub fn detect_return_value(
604 source: &str,
605 tree: &Tree,
606 start_byte: usize,
607 end_byte: usize,
608 enclosing_fn_end_byte: Option<usize>,
609 lang: LangId,
610) -> Result<ReturnKind, ReturnValueError> {
611 let root = tree.root_node();
612 let effective_enclosing_fn_end_byte = find_enclosing_function(&root, start_byte, lang)
613 .map(|node| node.end_byte())
614 .or(enclosing_fn_end_byte);
615
616 let in_range_bindings =
617 collect_return_bindings_in_range(&root, source, start_byte, end_byte, lang);
618 let mut live_bindings = Vec::new();
619
620 if let Some(fn_end) = effective_enclosing_fn_end_byte {
622 let post_range_end = fn_end.min(source.len());
623 if end_byte < post_range_end {
624 let mut post_refs: Vec<String> = Vec::new();
625 collect_identifier_refs(
626 &root,
627 source,
628 end_byte,
629 post_range_end,
630 lang,
631 &mut post_refs,
632 );
633
634 let mut seen = HashSet::new();
635 for binding in in_range_bindings {
636 let live_names = binding
637 .bound_names
638 .iter()
639 .filter(|name| post_refs.contains(name))
640 .cloned()
641 .collect::<Vec<_>>();
642 if live_names.is_empty() {
643 continue;
644 }
645 if !binding.representable {
646 return Err(ReturnValueError {
647 message: format!(
648 "extract_function cannot preserve unsupported live-out binding shape '{}' (bindings: {})",
649 binding.name,
650 live_names.join(", ")
651 ),
652 });
653 }
654 if seen.insert(binding.name.clone()) {
655 live_bindings.push(binding);
656 }
657 }
658 }
659 }
660
661 if let Some(expr) = find_return_in_range(&root, source, start_byte, end_byte) {
662 if !live_bindings.is_empty() {
663 return Err(ReturnValueError {
664 message: format!(
665 "extract_function cannot preserve explicit return together with live-out bindings: {}",
666 live_bindings
667 .iter()
668 .map(|binding| binding.name.as_str())
669 .collect::<Vec<_>>()
670 .join(", ")
671 ),
672 });
673 }
674 return Ok(ReturnKind::Expression(expr));
675 }
676
677 match live_bindings.as_slice() {
678 [] => Ok(ReturnKind::Void),
679 [binding] => Ok(ReturnKind::Variable(binding.encoded_for_return_kind())),
680 bindings => {
681 if matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) {
682 let group = bindings[0].js_kind.multi_value_group();
683 if bindings
684 .iter()
685 .any(|binding| binding.js_kind.multi_value_group() != group)
686 {
687 return Err(ReturnValueError {
688 message: format!(
689 "extract_function cannot preserve mixed live-out binding kinds: {}",
690 bindings
691 .iter()
692 .map(|binding| format!(
693 "{} ({})",
694 binding.name,
695 binding.js_kind.label()
696 ))
697 .collect::<Vec<_>>()
698 .join(", ")
699 ),
700 });
701 }
702 }
703 Ok(ReturnKind::Variables(
704 bindings
705 .iter()
706 .map(ReturnVariableBinding::encoded_for_return_kind)
707 .collect(),
708 ))
709 }
710 }
711}
712
713fn collect_return_bindings_in_range(
715 node: &Node,
716 source: &str,
717 start_byte: usize,
718 end_byte: usize,
719 lang: LangId,
720) -> Vec<ReturnVariableBinding> {
721 let mut bindings = Vec::new();
722 collect_return_bindings_recursive(node, source, start_byte, end_byte, lang, &mut bindings);
723 bindings
724}
725
726fn collect_return_bindings_recursive(
727 node: &Node,
728 source: &str,
729 start_byte: usize,
730 end_byte: usize,
731 lang: LangId,
732 out: &mut Vec<ReturnVariableBinding>,
733) {
734 if node.end_byte() <= start_byte || node.start_byte() >= end_byte {
735 return;
736 }
737
738 match lang {
739 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
740 if node.kind() == "variable_declarator" {
741 if let Some(name_node) = node.child_by_field_name("name") {
742 if name_node.start_byte() >= start_byte && name_node.end_byte() <= end_byte {
743 let name = node_text(source, &name_node).to_string();
744 let bound_names = collect_binding_pattern_names(source, &name_node, lang);
745 out.push(ReturnVariableBinding {
746 representable: name_node.kind() == "identifier",
747 name,
748 bound_names,
749 js_kind: js_declaration_kind_for_declarator(node),
750 });
751 }
752 }
753 } else if is_assignment_node(node) {
754 if let Some(left) = node.child_by_field_name("left") {
755 if left.start_byte() >= start_byte && left.end_byte() <= end_byte {
756 let name = node_text(source, &left).to_string();
757 out.push(ReturnVariableBinding {
758 representable: left.kind() == "identifier",
759 bound_names: collect_binding_pattern_names(source, &left, lang),
760 name,
761 js_kind: JsDeclarationKind::Assignment,
762 });
763 }
764 }
765 }
766 }
767 LangId::Python => {
768 if node.kind() == "assignment" {
769 if let Some(left) = node.child_by_field_name("left") {
770 if left.start_byte() >= start_byte && left.end_byte() <= end_byte {
771 let name = node_text(source, &left).to_string();
772 out.push(ReturnVariableBinding {
773 representable: left.kind() == "identifier",
774 bound_names: collect_binding_pattern_names(source, &left, lang),
775 name,
776 js_kind: JsDeclarationKind::Assignment,
777 });
778 }
779 }
780 }
781 }
782 _ => {}
783 }
784
785 let child_count = node.child_count();
786 for i in 0..child_count {
787 if let Some(child) = node.child(i as u32) {
788 collect_return_bindings_recursive(&child, source, start_byte, end_byte, lang, out);
789 }
790 }
791}
792
793fn collect_binding_pattern_names(source: &str, node: &Node, lang: LangId) -> Vec<String> {
794 fn collect(source: &str, node: &Node, lang: LangId, out: &mut Vec<String>) {
795 match node.kind() {
796 "identifier" | "shorthand_property_identifier_pattern" => {
797 let name = node_text(source, node).to_string();
798 if !out.contains(&name) {
799 out.push(name);
800 }
801 }
802 "pair_pattern" => {
803 if let Some(value) = node.child_by_field_name("value") {
804 collect(source, &value, lang, out);
805 }
806 }
807 "assignment_pattern" => {
808 if let Some(left) = node.child_by_field_name("left") {
809 collect(source, &left, lang, out);
810 }
811 }
812 "object_pattern" | "array_pattern" | "rest_pattern"
813 if matches!(lang, LangId::TypeScript | LangId::Tsx | LangId::JavaScript) =>
814 {
815 for index in 0..node.named_child_count() as u32 {
816 if let Some(child) = node.named_child(index) {
817 collect(source, &child, lang, out);
818 }
819 }
820 }
821 "pattern_list" | "tuple_pattern" | "list_pattern" | "list" | "tuple"
822 | "starred_expression"
823 if lang == LangId::Python =>
824 {
825 for index in 0..node.named_child_count() as u32 {
826 if let Some(child) = node.named_child(index) {
827 collect(source, &child, lang, out);
828 }
829 }
830 }
831 _ => {}
832 }
833 }
834
835 let mut names = Vec::new();
836 collect(source, node, lang, &mut names);
837 names
838}
839
840fn js_declaration_kind_for_declarator(node: &Node) -> JsDeclarationKind {
841 let Some(parent) = node.parent() else {
842 return JsDeclarationKind::Const;
843 };
844
845 match parent.kind() {
846 "variable_declaration" => JsDeclarationKind::Var,
847 "lexical_declaration" => {
848 let mut cursor = parent.walk();
849 if cursor.goto_first_child() {
850 loop {
851 let child = cursor.node();
852 match child.kind() {
853 "let" => return JsDeclarationKind::Let,
854 "const" => return JsDeclarationKind::Const,
855 _ => {}
856 }
857 if !cursor.goto_next_sibling() {
858 break;
859 }
860 }
861 }
862 JsDeclarationKind::Const
863 }
864 _ => JsDeclarationKind::Const,
865 }
866}
867
868fn is_assignment_node(node: &Node) -> bool {
869 matches!(
870 node.kind(),
871 "assignment_expression" | "augmented_assignment_expression" | "assignment"
872 )
873}
874
875fn find_return_in_range(
877 node: &Node,
878 source: &str,
879 start_byte: usize,
880 end_byte: usize,
881) -> Option<String> {
882 if node.end_byte() <= start_byte || node.start_byte() >= end_byte {
883 return None;
884 }
885
886 if node.kind() == "return_statement"
887 && node.start_byte() >= start_byte
888 && node.end_byte() <= end_byte
889 {
890 let text = node_text(source, node).trim().to_string();
892 let expr = text
893 .strip_prefix("return")
894 .unwrap_or("")
895 .trim()
896 .trim_end_matches(';')
897 .trim()
898 .to_string();
899 if !expr.is_empty() {
900 return Some(expr);
901 }
902 }
903
904 let mut cursor = node.walk();
905 if cursor.goto_first_child() {
906 loop {
907 if let Some(result) = find_return_in_range(&cursor.node(), source, start_byte, end_byte)
908 {
909 return Some(result);
910 }
911 if !cursor.goto_next_sibling() {
912 break;
913 }
914 }
915 }
916
917 None
918}
919
920pub fn generate_extracted_function(
926 name: &str,
927 params: &[String],
928 return_kind: &ReturnKind,
929 body_text: &str,
930 base_indent: &str,
931 lang: LangId,
932 indent_style: IndentStyle,
933) -> String {
934 let indent_unit = indent_style.as_str();
935
936 match lang {
937 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => generate_ts_function(
938 name,
939 params,
940 return_kind,
941 body_text,
942 base_indent,
943 indent_unit,
944 ),
945 LangId::Python => generate_py_function(
946 name,
947 params,
948 return_kind,
949 body_text,
950 base_indent,
951 indent_unit,
952 ),
953 _ => {
954 generate_ts_function(
956 name,
957 params,
958 return_kind,
959 body_text,
960 base_indent,
961 indent_unit,
962 )
963 }
964 }
965}
966
967fn generate_ts_function(
968 name: &str,
969 params: &[String],
970 return_kind: &ReturnKind,
971 body_text: &str,
972 base_indent: &str,
973 indent_unit: &str,
974) -> String {
975 let params_str = params.join(", ");
976 let mut lines = Vec::new();
977
978 lines.push(format!(
979 "{}function {}({}) {{",
980 base_indent, name, params_str
981 ));
982
983 let common_indent = common_leading_indent(body_text);
985 for line in body_text.lines() {
986 if line.trim().is_empty() {
987 lines.push(String::new());
988 } else {
989 let body_line = strip_leading_indent(line, &common_indent);
990 lines.push(format!("{}{}{}", base_indent, indent_unit, body_line));
991 }
992 }
993
994 match return_kind {
996 ReturnKind::Variable(var) => {
997 let binding = parse_return_variable(var);
998 lines.push(format!(
999 "{}{}return {};",
1000 base_indent, indent_unit, binding.name
1001 ));
1002 }
1003 ReturnKind::Variables(vars) => {
1004 let names = vars
1005 .iter()
1006 .map(|var| parse_return_variable(var).name)
1007 .collect::<Vec<_>>()
1008 .join(", ");
1009 lines.push(format!(
1010 "{}{}return {{ {} }};",
1011 base_indent, indent_unit, names
1012 ));
1013 }
1014 ReturnKind::Expression(_) => {
1015 }
1017 ReturnKind::Void => {}
1018 }
1019
1020 lines.push(format!("{}}}", base_indent));
1021 lines.join("\n")
1022}
1023
1024fn generate_py_function(
1025 name: &str,
1026 params: &[String],
1027 return_kind: &ReturnKind,
1028 body_text: &str,
1029 base_indent: &str,
1030 indent_unit: &str,
1031) -> String {
1032 let params_str = params.join(", ");
1033 let mut lines = Vec::new();
1034
1035 lines.push(format!("{}def {}({}):", base_indent, name, params_str));
1036
1037 let common_indent = common_leading_indent(body_text);
1039 for line in body_text.lines() {
1040 if line.trim().is_empty() {
1041 lines.push(String::new());
1042 } else {
1043 let body_line = strip_leading_indent(line, &common_indent);
1044 lines.push(format!("{}{}{}", base_indent, indent_unit, body_line));
1045 }
1046 }
1047
1048 match return_kind {
1050 ReturnKind::Variable(var) => {
1051 let binding = parse_return_variable(var);
1052 lines.push(format!(
1053 "{}{}return {}",
1054 base_indent, indent_unit, binding.name
1055 ));
1056 }
1057 ReturnKind::Variables(vars) => {
1058 let names = vars
1059 .iter()
1060 .map(|var| parse_return_variable(var).name)
1061 .collect::<Vec<_>>()
1062 .join(", ");
1063 lines.push(format!("{}{}return {}", base_indent, indent_unit, names));
1064 }
1065 ReturnKind::Expression(_) => {
1066 }
1068 ReturnKind::Void => {}
1069 }
1070
1071 lines.join("\n")
1072}
1073
1074fn common_leading_indent(text: &str) -> String {
1075 let mut lines = text.lines().filter(|line| !line.trim().is_empty());
1076 let Some(first) = lines.next() else {
1077 return String::new();
1078 };
1079
1080 let mut common = leading_whitespace(first).to_string();
1081 for line in lines {
1082 let indent = leading_whitespace(line);
1083 let common_len = common
1084 .char_indices()
1085 .zip(indent.char_indices())
1086 .take_while(|((_, left), (_, right))| left == right)
1087 .map(|((idx, ch), _)| idx + ch.len_utf8())
1088 .last()
1089 .unwrap_or(0);
1090 common.truncate(common_len);
1091 if common.is_empty() {
1092 break;
1093 }
1094 }
1095
1096 common
1097}
1098
1099fn leading_whitespace(line: &str) -> &str {
1100 let trimmed = line.trim_start_matches(|ch: char| ch == ' ' || ch == '\t');
1101 &line[..line.len() - trimmed.len()]
1102}
1103
1104fn strip_leading_indent<'a>(line: &'a str, indent: &str) -> &'a str {
1105 if indent.is_empty() {
1106 line
1107 } else {
1108 line.strip_prefix(indent).unwrap_or(line)
1109 }
1110}
1111
1112pub fn generate_call_site(
1114 name: &str,
1115 params: &[String],
1116 return_kind: &ReturnKind,
1117 indent: &str,
1118 lang: LangId,
1119) -> String {
1120 let args_str = params.join(", ");
1121
1122 match return_kind {
1123 ReturnKind::Variable(var) => match lang {
1124 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
1125 let binding = parse_return_variable(var);
1126 match binding.js_kind {
1127 JsDeclarationKind::Const => {
1128 format!("{}const {} = {}({});", indent, binding.name, name, args_str)
1129 }
1130 JsDeclarationKind::Let => {
1131 format!("{}let {} = {}({});", indent, binding.name, name, args_str)
1132 }
1133 JsDeclarationKind::Var => {
1134 format!("{}var {} = {}({});", indent, binding.name, name, args_str)
1135 }
1136 JsDeclarationKind::Assignment => {
1137 format!("{}{} = {}({});", indent, binding.name, name, args_str)
1138 }
1139 }
1140 }
1141 LangId::Python => {
1142 let binding = parse_return_variable(var);
1143 format!("{}{} = {}({})", indent, binding.name, name, args_str)
1144 }
1145 _ => format!("{}const {} = {}({});", indent, var, name, args_str),
1146 },
1147 ReturnKind::Variables(vars) => {
1148 let bindings = vars
1149 .iter()
1150 .map(|var| parse_return_variable(var))
1151 .collect::<Vec<_>>();
1152 let names = bindings
1153 .iter()
1154 .map(|binding| binding.name.as_str())
1155 .collect::<Vec<_>>()
1156 .join(", ");
1157 match lang {
1158 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
1159 let declaration = match bindings[0].js_kind.multi_value_group() {
1160 0 => "const ",
1161 1 => "let ",
1162 2 => "",
1163 _ => unreachable!(),
1164 };
1165 if declaration.is_empty() {
1166 format!("{}({{ {} }} = {}({}));", indent, names, name, args_str)
1167 } else {
1168 format!(
1169 "{}{}{{ {} }} = {}({});",
1170 indent, declaration, names, name, args_str
1171 )
1172 }
1173 }
1174 LangId::Python => format!("{}{} = {}({})", indent, names, name, args_str),
1175 _ => format!("{}const {{ {} }} = {}({});", indent, names, name, args_str),
1176 }
1177 }
1178 ReturnKind::Expression(_expr) => match lang {
1179 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
1180 format!("{}return {}({});", indent, name, args_str)
1181 }
1182 LangId::Python => {
1183 format!("{}return {}({})", indent, name, args_str)
1184 }
1185 _ => format!("{}return {}({});", indent, name, args_str),
1186 },
1187 ReturnKind::Void => match lang {
1188 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
1189 format!("{}{}({});", indent, name, args_str)
1190 }
1191 LangId::Python => {
1192 format!("{}{}({})", indent, name, args_str)
1193 }
1194 _ => format!("{}{}({});", indent, name, args_str),
1195 },
1196 }
1197}
1198
1199#[derive(Debug, Clone, PartialEq, Eq)]
1205pub struct ScopeConflict {
1206 pub name: String,
1208 pub suggested: String,
1210}
1211
1212pub fn detect_scope_conflicts(
1219 source: &str,
1220 tree: &Tree,
1221 insertion_byte: usize,
1222 _param_names: &[String],
1223 body_text: &str,
1224 lang: LangId,
1225) -> Vec<ScopeConflict> {
1226 let root = tree.root_node();
1227
1228 let enclosing_fn = find_enclosing_function(&root, insertion_byte, lang);
1230
1231 let mut scope_decls: HashSet<String> = HashSet::new();
1233 if let Some(fn_node) = enclosing_fn {
1234 collect_declarations_in_range(
1235 &fn_node,
1236 source,
1237 fn_node.start_byte(),
1238 fn_node.end_byte(),
1239 lang,
1240 &mut scope_decls,
1241 );
1242 collect_function_params(&fn_node, source, lang, &mut scope_decls);
1243 } else {
1244 collect_declarations_in_range(
1246 &root,
1247 source,
1248 root.start_byte(),
1249 root.end_byte(),
1250 lang,
1251 &mut scope_decls,
1252 );
1253 }
1254
1255 let mut body_decls: HashSet<String> = HashSet::new();
1257 let body_grammar = grammar_for(lang);
1258 let mut body_parser = tree_sitter::Parser::new();
1259 if body_parser.set_language(&body_grammar).is_ok() {
1260 if let Some(body_tree) = body_parser.parse(body_text.as_bytes(), None) {
1261 let body_root = body_tree.root_node();
1262 collect_declarations_in_range(
1263 &body_root,
1264 body_text,
1265 0,
1266 body_text.len(),
1267 lang,
1268 &mut body_decls,
1269 );
1270 }
1271 }
1272
1273 let mut conflicts = Vec::new();
1275 for decl in &body_decls {
1276 if scope_decls.contains(decl) {
1277 conflicts.push(ScopeConflict {
1278 name: decl.clone(),
1279 suggested: format!("{}_inlined", decl),
1280 });
1281 }
1282 }
1283
1284 conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1286 conflicts
1287}
1288
1289pub fn validate_single_return(
1296 source: &str,
1297 _tree: &Tree,
1298 fn_node: &Node,
1299 lang: LangId,
1300) -> Result<(), usize> {
1301 if lang != LangId::Python && fn_node.kind() == "arrow_function" {
1303 if let Some(body) = fn_node.child_by_field_name("body") {
1304 if body.kind() != "statement_block" {
1305 return Ok(());
1307 }
1308 }
1309 }
1310
1311 let count = count_return_statements(fn_node, source);
1312 if count > 1 {
1313 Err(count)
1314 } else {
1315 Ok(())
1316 }
1317}
1318
1319fn count_return_statements(node: &Node, source: &str) -> usize {
1321 let _ = source;
1322 let mut count = 0;
1323
1324 let nested_fn_kinds = [
1326 "function_declaration",
1327 "function_definition",
1328 "arrow_function",
1329 "method_definition",
1330 ];
1331
1332 let kind = node.kind();
1333 if kind == "return_statement" {
1334 return 1;
1335 }
1336
1337 let child_count = node.child_count();
1338 for i in 0..child_count {
1339 if let Some(child) = node.child(i as u32) {
1340 if nested_fn_kinds.contains(&child.kind()) {
1342 continue;
1343 }
1344 count += count_return_statements(&child, source);
1345 }
1346 }
1347
1348 count
1349}
1350
1351pub fn substitute_params(
1357 body_text: &str,
1358 param_to_arg: &std::collections::HashMap<String, String>,
1359 lang: LangId,
1360) -> String {
1361 if param_to_arg.is_empty() {
1362 return body_text.to_string();
1363 }
1364
1365 let grammar = grammar_for(lang);
1366 let mut parser = tree_sitter::Parser::new();
1367 if parser.set_language(&grammar).is_err() {
1368 return body_text.to_string();
1369 }
1370
1371 let tree = match parser.parse(body_text.as_bytes(), None) {
1372 Some(t) => t,
1373 None => return body_text.to_string(),
1374 };
1375
1376 let mut replacements: Vec<(usize, usize, String)> = Vec::new();
1379 let shadowed = HashSet::new();
1380 collect_param_replacements(
1381 &tree.root_node(),
1382 body_text,
1383 param_to_arg,
1384 lang,
1385 &shadowed,
1386 true,
1387 &mut replacements,
1388 );
1389
1390 replacements.sort_by(|a, b| b.0.cmp(&a.0));
1392
1393 let mut result = body_text.to_string();
1394 for (start, end, replacement) in replacements {
1395 result = format!("{}{}{}", &result[..start], replacement, &result[end..]);
1396 }
1397
1398 result
1399}
1400
1401fn collect_param_replacements(
1403 node: &Node,
1404 source: &str,
1405 param_to_arg: &std::collections::HashMap<String, String>,
1406 lang: LangId,
1407 shadowed: &HashSet<String>,
1408 is_root: bool,
1409 out: &mut Vec<(usize, usize, String)>,
1410) {
1411 if !is_root && is_function_scope_node(node, lang) {
1412 return;
1413 }
1414
1415 let mut current_shadowed = shadowed.clone();
1416 collect_shadowing_bindings_in_scope(node, source, param_to_arg, lang, &mut current_shadowed);
1417
1418 let kind = node.kind();
1419
1420 if kind == "identifier" {
1421 if !is_property_access(node, lang) && !is_binding_identifier(node) {
1423 let name = node_text(source, node);
1424 if !current_shadowed.contains(name) {
1425 if let Some(replacement) = param_to_arg.get(name) {
1426 out.push((node.start_byte(), node.end_byte(), replacement.clone()));
1427 }
1428 }
1429 }
1430 }
1431
1432 let child_count = node.child_count();
1435 for i in 0..child_count {
1436 if let Some(child) = node.child(i as u32) {
1437 collect_param_replacements(
1438 &child,
1439 source,
1440 param_to_arg,
1441 lang,
1442 ¤t_shadowed,
1443 false,
1444 out,
1445 );
1446 }
1447 }
1448}
1449
1450fn collect_shadowing_bindings_in_scope(
1451 scope: &Node,
1452 source: &str,
1453 param_to_arg: &std::collections::HashMap<String, String>,
1454 lang: LangId,
1455 out: &mut HashSet<String>,
1456) {
1457 collect_shadowing_bindings_in_scope_recursive(
1458 scope,
1459 scope.id(),
1460 source,
1461 param_to_arg,
1462 lang,
1463 out,
1464 );
1465}
1466
1467fn collect_shadowing_bindings_in_scope_recursive(
1468 node: &Node,
1469 scope_id: usize,
1470 source: &str,
1471 param_to_arg: &std::collections::HashMap<String, String>,
1472 lang: LangId,
1473 out: &mut HashSet<String>,
1474) {
1475 if node.id() != scope_id {
1476 if is_function_scope_node(node, lang) || is_block_scope_node(node, lang) {
1477 return;
1478 }
1479 }
1480
1481 match node.kind() {
1482 "variable_declarator" => {
1483 if let Some(name) = node.child_by_field_name("name") {
1484 collect_shadowing_names_from_pattern(&name, source, param_to_arg, out);
1485 }
1486 }
1487 "catch_clause" => {
1488 if let Some(parameter) = node.child_by_field_name("parameter") {
1489 collect_shadowing_names_from_pattern(¶meter, source, param_to_arg, out);
1490 }
1491 }
1492 "for_in_statement" | "for_of_statement" => {
1493 if let Some(left) = node.child_by_field_name("left") {
1494 collect_shadowing_names_from_pattern(&left, source, param_to_arg, out);
1495 }
1496 }
1497 "assignment" if lang == LangId::Python => {
1498 if let Some(left) = node.child_by_field_name("left") {
1499 collect_shadowing_names_from_pattern(&left, source, param_to_arg, out);
1500 }
1501 }
1502 _ => {}
1503 }
1504
1505 let child_count = node.child_count();
1506 for i in 0..child_count {
1507 if let Some(child) = node.child(i as u32) {
1508 collect_shadowing_bindings_in_scope_recursive(
1509 &child,
1510 scope_id,
1511 source,
1512 param_to_arg,
1513 lang,
1514 out,
1515 );
1516 }
1517 }
1518}
1519
1520fn collect_shadowing_names_from_pattern(
1521 node: &Node,
1522 source: &str,
1523 param_to_arg: &std::collections::HashMap<String, String>,
1524 out: &mut HashSet<String>,
1525) {
1526 if node.kind() == "identifier" {
1527 let name = node_text(source, node);
1528 if param_to_arg.contains_key(name) {
1529 out.insert(name.to_string());
1530 }
1531 return;
1532 }
1533
1534 let child_count = node.child_count();
1535 for i in 0..child_count {
1536 if let Some(child) = node.child(i as u32) {
1537 collect_shadowing_names_from_pattern(&child, source, param_to_arg, out);
1538 }
1539 }
1540}
1541
1542fn is_function_scope_node(node: &Node, lang: LangId) -> bool {
1543 match lang {
1544 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => matches!(
1545 node.kind(),
1546 "function_declaration" | "method_definition" | "arrow_function" | "function_expression"
1547 ),
1548 LangId::Python => node.kind() == "function_definition" || node.kind() == "lambda",
1549 _ => false,
1550 }
1551}
1552
1553fn is_block_scope_node(node: &Node, lang: LangId) -> bool {
1554 match lang {
1555 LangId::TypeScript | LangId::Tsx | LangId::JavaScript => node.kind() == "statement_block",
1556 LangId::Python => node.kind() == "block",
1557 _ => false,
1558 }
1559}
1560
1561fn is_binding_identifier(node: &Node) -> bool {
1562 let Some(parent) = node.parent() else {
1563 return false;
1564 };
1565
1566 if let Some(name) = parent.child_by_field_name("name") {
1567 if name.id() == node.id() || node_is_inside(&name, node) {
1568 return true;
1569 }
1570 }
1571 if let Some(pattern) = parent.child_by_field_name("pattern") {
1572 if pattern.id() == node.id() || node_is_inside(&pattern, node) {
1573 return true;
1574 }
1575 }
1576 if let Some(parameter) = parent.child_by_field_name("parameter") {
1577 if parameter.id() == node.id() || node_is_inside(¶meter, node) {
1578 return true;
1579 }
1580 }
1581 if let Some(left) = parent.child_by_field_name("left") {
1582 if matches!(
1583 parent.kind(),
1584 "for_in_statement" | "for_of_statement" | "assignment"
1585 ) && (left.id() == node.id() || node_is_inside(&left, node))
1586 {
1587 return true;
1588 }
1589 }
1590
1591 false
1592}
1593
1594fn node_is_inside(container: &Node, node: &Node) -> bool {
1595 container.start_byte() <= node.start_byte() && node.end_byte() <= container.end_byte()
1596}
1597
1598#[cfg(test)]
1603mod tests {
1604 use super::*;
1605 use crate::parser::grammar_for;
1606 use std::path::PathBuf;
1607 use tree_sitter::Parser;
1608
1609 fn fixture_path(name: &str) -> PathBuf {
1610 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1611 .join("tests")
1612 .join("fixtures")
1613 .join("extract_function")
1614 .join(name)
1615 }
1616
1617 fn parse_source(source: &str, lang: LangId) -> Tree {
1618 let grammar = grammar_for(lang);
1619 let mut parser = Parser::new();
1620 parser.set_language(&grammar).unwrap();
1621 parser.parse(source.as_bytes(), None).unwrap()
1622 }
1623
1624 #[test]
1627 fn free_vars_detects_enclosing_function_params() {
1628 let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1630 let tree = parse_source(&source, LangId::TypeScript);
1631
1632 let line5_start = crate::edit::line_col_to_byte(&source, 5, 0);
1637 let line6_end = crate::edit::line_col_to_byte(&source, 7, 0);
1638
1639 let result =
1640 detect_free_variables(&source, &tree, line5_start, line6_end, LangId::TypeScript);
1641 assert!(
1642 result.parameters.contains(&"items".to_string()),
1643 "should detect 'items' as parameter, got: {:?}",
1644 result.parameters
1645 );
1646 assert!(
1647 result.parameters.contains(&"prefix".to_string()),
1648 "should detect 'prefix' as parameter, got: {:?}",
1649 result.parameters
1650 );
1651 assert!(!result.has_this_or_self);
1652 }
1653
1654 #[test]
1657 fn free_vars_filters_property_identifiers() {
1658 let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1661 let tree = parse_source(&source, LangId::TypeScript);
1662
1663 let line5_start = crate::edit::line_col_to_byte(&source, 5, 0);
1664 let line6_end = crate::edit::line_col_to_byte(&source, 7, 0);
1665
1666 let result =
1667 detect_free_variables(&source, &tree, line5_start, line6_end, LangId::TypeScript);
1668 assert!(
1670 !result.parameters.contains(&"filter".to_string()),
1671 "property 'filter' should not be a free variable"
1672 );
1673 assert!(
1674 !result.parameters.contains(&"length".to_string()),
1675 "property 'length' should not be a free variable"
1676 );
1677 assert!(
1678 !result.parameters.contains(&"map".to_string()),
1679 "property 'map' should not be a free variable"
1680 );
1681 }
1682
1683 #[test]
1686 fn free_vars_skips_module_level_refs() {
1687 let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1690 let tree = parse_source(&source, LangId::TypeScript);
1691
1692 let start = crate::edit::line_col_to_byte(&source, 5, 0);
1694 let end = crate::edit::line_col_to_byte(&source, 10, 0);
1695
1696 let result = detect_free_variables(&source, &tree, start, end, LangId::TypeScript);
1697 assert!(
1698 !result.parameters.contains(&"BASE_URL".to_string()),
1699 "module-level 'BASE_URL' should not be a parameter, got: {:?}",
1700 result.parameters
1701 );
1702 assert!(
1703 !result.parameters.contains(&"console".to_string()),
1704 "'console' should not be a parameter, got: {:?}",
1705 result.parameters
1706 );
1707 }
1708
1709 #[test]
1710 fn free_vars_plain_lexical_declaration_uses_real_enclosing_function() {
1711 let source = "function f(a: number) {\n const x = a + 1;\n return x;\n}\n";
1712 let tree = parse_source(source, LangId::TypeScript);
1713 let start = crate::edit::line_col_to_byte(source, 1, 0);
1714 let end = crate::edit::line_col_to_byte(source, 2, 0);
1715
1716 let result = detect_free_variables(source, &tree, start, end, LangId::TypeScript);
1717 assert!(
1718 result.parameters.contains(&"a".to_string()),
1719 "plain const declaration should not stop enclosing-function lookup: {:?}",
1720 result.parameters
1721 );
1722 }
1723
1724 #[test]
1727 fn free_vars_detects_this_in_ts() {
1728 let source = std::fs::read_to_string(fixture_path("sample_this.ts")).unwrap();
1729 let tree = parse_source(&source, LangId::TypeScript);
1730
1731 let start = crate::edit::line_col_to_byte(&source, 4, 0);
1733 let end = crate::edit::line_col_to_byte(&source, 7, 0);
1734
1735 let result = detect_free_variables(&source, &tree, start, end, LangId::TypeScript);
1736 assert!(result.has_this_or_self, "should detect 'this' reference");
1737 }
1738
1739 #[test]
1740 fn free_vars_detects_self_in_python() {
1741 let source = r#"
1742class UserService:
1743 def get_user(self, id):
1744 key = id.lower()
1745 user = self.users.get(key)
1746 return user
1747"#;
1748 let tree = parse_source(source, LangId::Python);
1749
1750 let start = crate::edit::line_col_to_byte(source, 4, 0);
1752 let end = crate::edit::line_col_to_byte(source, 5, 0);
1753
1754 let result = detect_free_variables(source, &tree, start, end, LangId::Python);
1755 assert!(result.has_this_or_self, "should detect 'self' reference");
1756 }
1757
1758 #[test]
1761 fn return_value_explicit_return() {
1762 let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1763 let tree = parse_source(&source, LangId::TypeScript);
1764
1765 let start = crate::edit::line_col_to_byte(&source, 14, 0);
1767 let end = crate::edit::line_col_to_byte(&source, 17, 0);
1768
1769 let result =
1770 detect_return_value(&source, &tree, start, end, None, LangId::TypeScript).unwrap();
1771 assert_eq!(result, ReturnKind::Expression("added".to_string()));
1772 }
1773
1774 #[test]
1775 fn return_value_post_range_usage() {
1776 let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1777 let tree = parse_source(&source, LangId::TypeScript);
1778
1779 let start = crate::edit::line_col_to_byte(&source, 5, 0);
1784 let end = crate::edit::line_col_to_byte(&source, 6, 0);
1785
1786 let fn_end = crate::edit::line_col_to_byte(&source, 10, 0);
1788
1789 let result =
1790 detect_return_value(&source, &tree, start, end, Some(fn_end), LangId::TypeScript)
1791 .unwrap();
1792 assert_eq!(result, ReturnKind::Variable("filtered".to_string()));
1794 }
1795
1796 #[test]
1797 fn return_value_void() {
1798 let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1799 let tree = parse_source(&source, LangId::TypeScript);
1800
1801 let start = crate::edit::line_col_to_byte(&source, 20, 0);
1803 let end = crate::edit::line_col_to_byte(&source, 22, 0);
1804
1805 let result = detect_return_value(
1806 &source,
1807 &tree,
1808 start,
1809 end,
1810 Some(crate::edit::line_col_to_byte(&source, 23, 0)),
1811 LangId::TypeScript,
1812 )
1813 .unwrap();
1814 assert_eq!(result, ReturnKind::Void);
1815 }
1816
1817 #[test]
1818 fn return_value_collects_multiple_const_live_outs_in_order() {
1819 let source = "function f() {\n const a = 1;\n const b = 2;\n return a + b;\n}\n";
1820 let tree = parse_source(source, LangId::TypeScript);
1821 let start = crate::edit::line_col_to_byte(source, 1, 0);
1822 let end = crate::edit::line_col_to_byte(source, 3, 0);
1823
1824 let result =
1825 detect_return_value(source, &tree, start, end, None, LangId::TypeScript).unwrap();
1826
1827 assert_eq!(
1828 result,
1829 ReturnKind::Variables(vec!["a".to_string(), "b".to_string()])
1830 );
1831 assert_eq!(
1832 generate_extracted_function(
1833 "makeValues",
1834 &[],
1835 &result,
1836 " const a = 1;\n const b = 2;",
1837 "",
1838 LangId::TypeScript,
1839 IndentStyle::Spaces(2),
1840 ),
1841 "function makeValues() {\n const a = 1;\n const b = 2;\n return { a, b };\n}"
1842 );
1843 assert_eq!(
1844 generate_call_site("makeValues", &[], &result, " ", LangId::TypeScript),
1845 " const { a, b } = makeValues();"
1846 );
1847 }
1848
1849 #[test]
1850 fn return_value_generates_mutable_destructure_for_let_pair() {
1851 let source = "function f() {\n let a = 1;\n let b = 2;\n return a + b;\n}\n";
1852 let tree = parse_source(source, LangId::TypeScript);
1853 let start = crate::edit::line_col_to_byte(source, 1, 0);
1854 let end = crate::edit::line_col_to_byte(source, 3, 0);
1855
1856 let result =
1857 detect_return_value(source, &tree, start, end, None, LangId::TypeScript).unwrap();
1858
1859 assert_eq!(
1860 result,
1861 ReturnKind::Variables(vec!["let a".to_string(), "let b".to_string()])
1862 );
1863 assert_eq!(
1864 generate_call_site("makeValues", &[], &result, " ", LangId::TypeScript),
1865 " let { a, b } = makeValues();"
1866 );
1867 }
1868
1869 #[test]
1870 fn return_value_generates_python_tuple_pair() {
1871 let source = "def f():\n a = 1\n b = 2\n return a + b\n";
1872 let tree = parse_source(source, LangId::Python);
1873 let start = crate::edit::line_col_to_byte(source, 1, 0);
1874 let end = crate::edit::line_col_to_byte(source, 3, 0);
1875
1876 let result = detect_return_value(source, &tree, start, end, None, LangId::Python).unwrap();
1877
1878 assert_eq!(
1879 generate_extracted_function(
1880 "make_values",
1881 &[],
1882 &result,
1883 " a = 1\n b = 2",
1884 "",
1885 LangId::Python,
1886 IndentStyle::Spaces(4),
1887 ),
1888 "def make_values():\n a = 1\n b = 2\n return a, b"
1889 );
1890 assert_eq!(
1891 generate_call_site("make_values", &[], &result, " ", LangId::Python),
1892 " a, b = make_values()"
1893 );
1894 }
1895
1896 #[test]
1897 fn return_value_rejects_mixed_javascript_binding_kinds() {
1898 let source = "function f() {\n const a = 1;\n let b = 2;\n return a + b;\n}\n";
1899 let tree = parse_source(source, LangId::TypeScript);
1900 let start = crate::edit::line_col_to_byte(source, 1, 0);
1901 let end = crate::edit::line_col_to_byte(source, 3, 0);
1902
1903 let error =
1904 detect_return_value(source, &tree, start, end, None, LangId::TypeScript).unwrap_err();
1905
1906 assert!(error.message.contains("a (const)"), "{}", error.message);
1907 assert!(error.message.contains("b (let)"), "{}", error.message);
1908 }
1909
1910 #[test]
1911 fn return_value_rejects_destructured_live_out_binding() {
1912 let source = "function f(input) {\n const { a, b } = input;\n return a + b;\n}\n";
1913 let tree = parse_source(source, LangId::TypeScript);
1914 let start = crate::edit::line_col_to_byte(source, 1, 0);
1915 let end = crate::edit::line_col_to_byte(source, 2, 0);
1916
1917 let error =
1918 detect_return_value(source, &tree, start, end, None, LangId::TypeScript).unwrap_err();
1919
1920 assert!(error.message.contains("unsupported live-out binding shape"));
1921 assert!(error.message.contains("a, b"), "{}", error.message);
1922 }
1923
1924 #[test]
1925 fn single_live_out_generation_remains_byte_identical() {
1926 let return_kind = ReturnKind::Variable("let result".to_string());
1927 let extracted = generate_extracted_function(
1928 "computeInitial",
1929 &[],
1930 &return_kind,
1931 " let result = compute();",
1932 "",
1933 LangId::TypeScript,
1934 IndentStyle::Spaces(2),
1935 );
1936 let call = generate_call_site(
1937 "computeInitial",
1938 &[],
1939 &return_kind,
1940 " ",
1941 LangId::TypeScript,
1942 );
1943
1944 assert_eq!(
1945 format!("{extracted}\n\nfunction f() {{\n{call}\n return result;\n}}\n"),
1946 "function computeInitial() {\n let result = compute();\n return result;\n}\n\nfunction f() {\n let result = computeInitial();\n return result;\n}\n"
1947 );
1948 }
1949
1950 #[test]
1953 fn generate_ts_function_with_params() {
1954 let body = "const doubled = x * 2;\nconst added = doubled + 10;";
1955 let result = generate_extracted_function(
1956 "compute",
1957 &["x".to_string()],
1958 &ReturnKind::Variable("added".to_string()),
1959 body,
1960 "",
1961 LangId::TypeScript,
1962 IndentStyle::Spaces(2),
1963 );
1964 assert!(result.contains("function compute(x)"));
1965 assert!(result.contains("return added;"));
1966 assert!(result.contains("}"));
1967 }
1968
1969 #[test]
1970 fn generate_ts_function_preserves_relative_indentation() {
1971 let body = " for (const item of items) {\n if (item.active) {\n console.log(item.name);\n }\n }";
1972 let result = generate_extracted_function(
1973 "processItems",
1974 &["items".to_string()],
1975 &ReturnKind::Void,
1976 body,
1977 "",
1978 LangId::TypeScript,
1979 IndentStyle::Spaces(2),
1980 );
1981 assert_eq!(
1982 result,
1983 "function processItems(items) {\n for (const item of items) {\n if (item.active) {\n console.log(item.name);\n }\n }\n}"
1984 );
1985 }
1986
1987 #[test]
1988 fn generate_py_function_with_params() {
1989 let body = "doubled = x * 2\nadded = doubled + 10";
1990 let result = generate_extracted_function(
1991 "compute",
1992 &["x".to_string()],
1993 &ReturnKind::Variable("added".to_string()),
1994 body,
1995 "",
1996 LangId::Python,
1997 IndentStyle::Spaces(4),
1998 );
1999 assert!(result.contains("def compute(x):"));
2000 assert!(result.contains("return added"));
2001 }
2002
2003 #[test]
2004 fn generate_call_site_with_return_var() {
2005 let call = generate_call_site(
2006 "compute",
2007 &["x".to_string()],
2008 &ReturnKind::Variable("result".to_string()),
2009 " ",
2010 LangId::TypeScript,
2011 );
2012 assert_eq!(call, " const result = compute(x);");
2013 }
2014
2015 #[test]
2016 fn generate_call_site_preserves_let_return_var() {
2017 let call = generate_call_site(
2018 "compute",
2019 &[],
2020 &ReturnKind::Variable("let result".to_string()),
2021 " ",
2022 LangId::TypeScript,
2023 );
2024 assert_eq!(call, " let result = compute();");
2025 }
2026
2027 #[test]
2028 fn generate_call_site_void() {
2029 let call = generate_call_site(
2030 "doWork",
2031 &["a".to_string(), "b".to_string()],
2032 &ReturnKind::Void,
2033 " ",
2034 LangId::TypeScript,
2035 );
2036 assert_eq!(call, " doWork(a, b);");
2037 }
2038
2039 #[test]
2040 fn generate_call_site_return_expression() {
2041 let call = generate_call_site(
2042 "compute",
2043 &["x".to_string()],
2044 &ReturnKind::Expression("x * 2".to_string()),
2045 " ",
2046 LangId::TypeScript,
2047 );
2048 assert_eq!(call, " return compute(x);");
2049 }
2050
2051 #[test]
2054 fn free_vars_python_function_params() {
2055 let source = std::fs::read_to_string(fixture_path("sample.py")).unwrap();
2056 let tree = parse_source(&source, LangId::Python);
2057
2058 let start = crate::edit::line_col_to_byte(&source, 5, 0);
2060 let end = crate::edit::line_col_to_byte(&source, 7, 0);
2061
2062 let result = detect_free_variables(&source, &tree, start, end, LangId::Python);
2063 assert!(
2064 result.parameters.contains(&"items".to_string()),
2065 "should detect 'items': {:?}",
2066 result.parameters
2067 );
2068 assert!(
2069 result.parameters.contains(&"prefix".to_string()),
2070 "should detect 'prefix': {:?}",
2071 result.parameters
2072 );
2073 assert!(!result.has_this_or_self);
2074 }
2075
2076 #[test]
2079 fn validate_single_return_single() {
2080 let source =
2081 "function add(a: number, b: number): number {\n const sum = a + b;\n return sum;\n}";
2082 let tree = parse_source(source, LangId::TypeScript);
2083 let root = tree.root_node();
2084 let fn_node = root.child(0).unwrap(); assert!(validate_single_return(source, &tree, &fn_node, LangId::TypeScript).is_ok());
2086 }
2087
2088 #[test]
2089 fn validate_single_return_void() {
2090 let source = "function greet(name: string): void {\n console.log(name);\n}";
2091 let tree = parse_source(source, LangId::TypeScript);
2092 let root = tree.root_node();
2093 let fn_node = root.child(0).unwrap();
2094 assert!(validate_single_return(source, &tree, &fn_node, LangId::TypeScript).is_ok());
2095 }
2096
2097 #[test]
2098 fn validate_single_return_expression_body() {
2099 let source = "const double = (n: number): number => n * 2;";
2100 let tree = parse_source(source, LangId::TypeScript);
2101 let root = tree.root_node();
2102 let lex_decl = root.child(0).unwrap();
2104 let var_decl = lex_decl.child(1).unwrap(); let arrow = var_decl.child_by_field_name("value").unwrap();
2106 assert_eq!(arrow.kind(), "arrow_function");
2107 assert!(validate_single_return(source, &tree, &arrow, LangId::TypeScript).is_ok());
2108 }
2109
2110 #[test]
2111 fn validate_single_return_multiple() {
2112 let source = "function abs(x: number): number {\n if (x > 0) {\n return x;\n }\n return -x;\n}";
2113 let tree = parse_source(source, LangId::TypeScript);
2114 let root = tree.root_node();
2115 let fn_node = root.child(0).unwrap();
2116 let result = validate_single_return(source, &tree, &fn_node, LangId::TypeScript);
2117 assert!(result.is_err());
2118 assert_eq!(result.unwrap_err(), 2);
2119 }
2120
2121 #[test]
2124 fn scope_conflicts_none() {
2125 let source = "function main() {\n const x = 10;\n const y = add(x, 5);\n}";
2127 let tree = parse_source(source, LangId::TypeScript);
2128 let body_text = "const sum = a + b;";
2129 let call_byte = crate::edit::line_col_to_byte(source, 2, 0);
2130 let conflicts =
2131 detect_scope_conflicts(source, &tree, call_byte, &[], body_text, LangId::TypeScript);
2132 assert!(
2133 conflicts.is_empty(),
2134 "expected no conflicts, got: {:?}",
2135 conflicts
2136 );
2137 }
2138
2139 #[test]
2140 fn scope_conflicts_detected() {
2141 let source = "function main() {\n const temp = 99;\n const result = compute(5);\n}";
2143 let tree = parse_source(source, LangId::TypeScript);
2144 let body_text = "const temp = x * 2;\nconst result2 = temp + 10;";
2145 let call_byte = crate::edit::line_col_to_byte(source, 2, 0);
2146 let conflicts =
2147 detect_scope_conflicts(source, &tree, call_byte, &[], body_text, LangId::TypeScript);
2148 assert!(!conflicts.is_empty(), "expected conflict for 'temp'");
2149 assert!(
2150 conflicts.iter().any(|c| c.name == "temp"),
2151 "conflicts: {:?}",
2152 conflicts
2153 );
2154 assert!(
2155 conflicts.iter().any(|c| c.suggested == "temp_inlined"),
2156 "should suggest temp_inlined"
2157 );
2158 }
2159
2160 #[test]
2163 fn substitute_params_basic() {
2164 let body = "const sum = a + b;";
2165 let mut map = std::collections::HashMap::new();
2166 map.insert("a".to_string(), "x".to_string());
2167 map.insert("b".to_string(), "y".to_string());
2168 let result = substitute_params(body, &map, LangId::TypeScript);
2169 assert_eq!(result, "const sum = x + y;");
2170 }
2171
2172 #[test]
2173 fn substitute_params_whole_word() {
2174 let body = "const result = items.filter(i => i > 0);";
2176 let mut map = std::collections::HashMap::new();
2177 map.insert("i".to_string(), "index".to_string());
2178 let result = substitute_params(body, &map, LangId::TypeScript);
2179 assert_eq!(result, body);
2182 }
2183
2184 #[test]
2185 fn substitute_params_rewrites_outer_reference_not_shadowed_arrow_param() {
2186 let body = "return x + items.map(x => x + 1)[0];";
2187 let mut map = std::collections::HashMap::new();
2188 map.insert("x".to_string(), "5".to_string());
2189 let result = substitute_params(body, &map, LangId::TypeScript);
2190 assert_eq!(result, "return 5 + items.map(x => x + 1)[0];");
2191 }
2192
2193 #[test]
2194 fn substitute_params_noop_same_name() {
2195 let body = "const sum = x + y;";
2196 let mut map = std::collections::HashMap::new();
2197 map.insert("x".to_string(), "x".to_string());
2198 let result = substitute_params(body, &map, LangId::TypeScript);
2199 assert_eq!(result, "const sum = x + y;");
2200 }
2201
2202 #[test]
2203 fn substitute_params_empty_map() {
2204 let body = "const sum = a + b;";
2205 let map = std::collections::HashMap::new();
2206 let result = substitute_params(body, &map, LangId::TypeScript);
2207 assert_eq!(result, body);
2208 }
2209}