Skip to main content

aft/
extract.rs

1//! Shared extraction utilities for `extract_function` (and future `inline_symbol`).
2//!
3//! Provides:
4//! - `detect_free_variables` — classify identifier references in a byte range
5//! - `detect_return_value` — infer what the extracted function should return
6//! - `generate_extracted_function` — produce function text for TS/JS or Python
7
8use std::collections::HashSet;
9
10use tree_sitter::{Node, Tree};
11
12use crate::indent::IndentStyle;
13use crate::parser::{grammar_for, node_text, LangId};
14
15// ---------------------------------------------------------------------------
16// Free variable detection
17// ---------------------------------------------------------------------------
18
19/// Classification result for free variables in a selected byte range.
20#[derive(Debug)]
21pub struct FreeVariableResult {
22    /// Identifiers declared in an enclosing function scope that the range
23    /// references — these become parameters of the extracted function.
24    pub parameters: Vec<String>,
25    /// Whether `this` (JS/TS) or `self` (Python) appears in the range.
26    pub has_this_or_self: bool,
27}
28
29/// Walk the AST for a byte range and classify every identifier reference.
30///
31/// Classification rules:
32/// 1. Declared-in-range (local variable) → skip
33/// 2. Declared in enclosing function scope → parameter
34/// 3. Module-level or import → skip
35/// 4. `this` / `self` keyword → flag (error for extract_function)
36/// 5. `property_identifier` / `field_identifier` on the right side of `.` → skip
37///    (these are member accesses, not free variables)
38pub 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    // 1. Collect all identifiers referenced in the range (excluding property access)
48    let mut references: Vec<String> = Vec::new();
49    collect_identifier_refs(&root, source, start_byte, end_byte, lang, &mut references);
50
51    // 2. Collect declarations within the range (these are locals, not free)
52    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    // 3. Find the enclosing function scope boundary
56    let enclosing_fn = find_enclosing_function(&root, start_byte, lang);
57
58    // 4. Collect declarations in the enclosing function but outside the range
59    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, // only before the range
66            lang,
67            &mut enclosing_decls,
68        );
69        // Also collect function parameters
70        collect_function_params(&fn_node, source, lang, &mut enclosing_decls);
71    }
72
73    // 5. Check for this/self
74    let has_this_or_self = check_this_or_self(&root, source, start_byte, end_byte, lang);
75
76    // 6. Classify: a reference is a parameter if it's not a local decl,
77    //    IS declared in the enclosing function scope, and is not module-level.
78    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; // dedup
86        }
87        if enclosing_decls.contains(name) {
88            parameters.push(name.clone());
89        }
90        // If not in enclosing_decls, it's module-level or global — skip
91    }
92
93    FreeVariableResult {
94        parameters,
95        has_this_or_self,
96    }
97}
98
99/// Collect all `identifier` nodes in [start_byte, end_byte) that are genuine
100/// references (not property accesses on the right side of `.`).
101fn 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    // Skip nodes entirely outside the range
110    if node.end_byte() <= start_byte || node.start_byte() >= end_byte {
111        return;
112    }
113
114    let kind = node.kind();
115
116    // An `identifier` node in the range that is NOT a property/field access
117    if kind == "identifier" && node.start_byte() >= start_byte && node.end_byte() <= end_byte {
118        // Check parent: if parent is member_expression and this is the "property" field,
119        // it's a property access, not a free variable.
120        if !is_property_access(node, lang) {
121            let name = node_text(source, node).to_string();
122            // Skip language keywords that parse as identifiers
123            if !is_keyword(&name, lang) {
124                out.push(name);
125            }
126        }
127    }
128
129    // Recurse into children
130    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
141/// Check if an identifier node is a property access (right side of `.`).
142fn is_property_access(node: &Node, lang: LangId) -> bool {
143    // property_identifier and field_identifier are separate node kinds in TS/JS,
144    // so they won't even reach here. But for Python `attribute` access the child
145    // is still `identifier`.
146    if let Some(parent) = node.parent() {
147        let pk = parent.kind();
148        match lang {
149            LangId::TypeScript | LangId::Tsx | LangId::JavaScript => {
150                // member_expression: object.property — the "property" child
151                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                // attribute: object.attr — the "attribute" child
159                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
171/// Identifiers that are language keywords and should not be treated as free variables.
172fn 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
236/// Collect names declared (via variable declarations) within a byte range.
237fn 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            // variable_declarator has a "name" child that is the declared identifier
254            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            // assignment: left side
264            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    // Recurse
279    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
290/// Collect parameter names from a function node.
291fn 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            // function_declaration / arrow_function have "parameters" field
295            if let Some(params) = fn_node.child_by_field_name("parameters") {
296                collect_param_identifiers(&params, source, lang, out);
297            }
298            // For arrow functions inside lexical_declaration, drill down
299            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(&params, 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(&params, source, lang, out);
321            }
322        }
323        _ => {}
324    }
325}
326
327/// Walk a parameter list node and collect identifier names.
328fn 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                    // required_parameter, optional_parameter have pattern child,
341                    // or directly identifier
342                    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                        // Skip `self` parameter
357                        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
371/// Find the innermost function node that encloses `byte_pos`.
372fn 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
376/// Find the deepest function-like ancestor that contains `byte_pos`.
377fn 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
443/// Check if `this` (JS/TS) or `self` (Python) appears in the byte range.
444fn 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                    // Check it's not a parameter declaration (like `def foo(self):`)
466                    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// ---------------------------------------------------------------------------
494// Return value detection
495// ---------------------------------------------------------------------------
496
497/// What the extracted function should return.
498#[derive(Debug, Clone, PartialEq, Eq)]
499pub enum ReturnKind {
500    /// The range contains an explicit `return expr;` → use that expression
501    Expression(String),
502    /// A variable declared in-range is used after the range in the enclosing function
503    Variable(String),
504    /// Multiple variables declared in-range are used after the range.
505    Variables(Vec<String>),
506    /// Nothing needs to be returned (void)
507    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
598/// Detect what the extracted code range should return.
599///
600/// Every live-out binding must be represented in the generated call. Unsupported
601/// binding shapes and incompatible JavaScript declaration kinds are rejected
602/// rather than allowing an extraction to silently discard caller state.
603pub 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    // Check if any in-range declaration is used after the range in the enclosing fn
621    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
713/// Collect in-range names that can be returned to preserve post-range uses.
714fn 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
875/// Find an explicit `return` statement in the byte range and return its expression text.
876fn 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        // Get the expression after "return"
891        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
920// ---------------------------------------------------------------------------
921// Function generation
922// ---------------------------------------------------------------------------
923
924/// Generate the text for an extracted function.
925pub 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            // Shouldn't reach here due to language guard, but produce something reasonable
955            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    // Re-indent body to be inside the function while preserving relative nesting.
984    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    // Add return statement if needed
995    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            // The return is already in the body text
1016        }
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    // Re-indent body while preserving relative nesting.
1038    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    // Add return statement if needed
1049    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            // Already in body
1067        }
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
1112/// Generate the call site text that replaces the extracted range.
1113pub 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// ---------------------------------------------------------------------------
1200// Inline symbol utilities
1201// ---------------------------------------------------------------------------
1202
1203/// A detected scope conflict when inlining a function body at a call site.
1204#[derive(Debug, Clone, PartialEq, Eq)]
1205pub struct ScopeConflict {
1206    /// The variable name that conflicts.
1207    pub name: String,
1208    /// Suggested alternative name to avoid the conflict.
1209    pub suggested: String,
1210}
1211
1212/// Detect scope conflicts between the call site scope and the function body
1213/// being inlined.
1214///
1215/// Collects all variable declarations at the call site's scope level
1216/// (surrounding function body), then checks for collisions with variables
1217/// declared in `body_text`.
1218pub 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    // 1. Find the enclosing function at the call site
1229    let enclosing_fn = find_enclosing_function(&root, insertion_byte, lang);
1230
1231    // 2. Collect all declarations in the call site's scope
1232    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        // Module-level: collect all top-level declarations
1245        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    // 3. Collect declarations in the body being inlined
1256    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    // 4. Find collisions
1274    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    // Sort for deterministic output
1285    conflicts.sort_by(|a, b| a.name.cmp(&b.name));
1286    conflicts
1287}
1288
1289/// Validate that a function has at most one return statement (suitable for inlining).
1290///
1291/// - Arrow functions with expression bodies (no `return` keyword) → valid (single-return)
1292/// - Functions with 0 returns (void) → valid
1293/// - Functions with exactly 1 return → valid
1294/// - Functions with >1 return → invalid, returns the count
1295pub fn validate_single_return(
1296    source: &str,
1297    _tree: &Tree,
1298    fn_node: &Node,
1299    lang: LangId,
1300) -> Result<(), usize> {
1301    // Arrow functions with expression bodies are always single-return
1302    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                // Expression body — implicitly single-return
1306                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
1319/// Count `return_statement` nodes in a function body (non-recursive into nested functions).
1320fn count_return_statements(node: &Node, source: &str) -> usize {
1321    let _ = source;
1322    let mut count = 0;
1323
1324    // Don't count returns in nested function bodies
1325    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            // Skip nested function definitions
1341            if nested_fn_kinds.contains(&child.kind()) {
1342                continue;
1343            }
1344            count += count_return_statements(&child, source);
1345        }
1346    }
1347
1348    count
1349}
1350
1351/// Substitute parameter names with argument expressions in a function body.
1352///
1353/// Uses tree-sitter to find `identifier` nodes matching parameter names,
1354/// replacing from end to start to preserve byte offsets. Only replaces
1355/// whole-word matches (identifiers, not substrings).
1356pub 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    // Collect identifier references that are still bound to the inlined function's
1377    // parameters. Nested functions and local shadowing declarations are skipped.
1378    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    // Sort by start position descending so replacements don't shift offsets
1391    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
1401/// Collect identifier nodes that match parameter names for substitution.
1402fn 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        // Check it's not a property access
1422        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    // Also handle Python-specific name node
1433    // Recurse into children
1434    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                &current_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(&parameter, 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(&parameter, 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// ---------------------------------------------------------------------------
1599// Tests
1600// ---------------------------------------------------------------------------
1601
1602#[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    // --- Free variable detection: simple identifiers ---
1625
1626    #[test]
1627    fn free_vars_detects_enclosing_function_params() {
1628        // `items` and `prefix` are function params → should be detected as free variables
1629        let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1630        let tree = parse_source(&source, LangId::TypeScript);
1631
1632        // Lines 5-8 (0-indexed): the body of processData that uses `items` and `prefix`
1633        // "  const filtered = items.filter(item => item.length > 0);"
1634        // "  const mapped = filtered.map(item => prefix + item);"
1635        // These lines reference `items` and `prefix` from the function params.
1636        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    // --- Property access filtering ---
1655
1656    #[test]
1657    fn free_vars_filters_property_identifiers() {
1658        // In `items.filter(...)`, `filter` should NOT be a free variable.
1659        // In `item.length`, `length` should NOT be a free variable.
1660        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        // "filter", "map", "length" should NOT appear
1669        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    // --- Module-level vs function-level classification ---
1684
1685    #[test]
1686    fn free_vars_skips_module_level_refs() {
1687        // `BASE_URL` is module-level → should NOT be a parameter
1688        // `console` is a global → should NOT be a parameter
1689        let source = std::fs::read_to_string(fixture_path("sample.ts")).unwrap();
1690        let tree = parse_source(&source, LangId::TypeScript);
1691
1692        // processData body: lines 5-9
1693        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    // --- this/self detection ---
1725
1726    #[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        // getUser method body lines 4-6 contain `this.users.get(key)`
1732        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        // Lines 3-4 (0-indexed) contain `self.users.get(key)`
1751        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    // --- Return value detection ---
1759
1760    #[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        // simpleHelper: lines 13-16 — contains "return added;"
1766        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        // processData lines 5-7: declares `filtered`, `mapped`
1780        // Lines 7-9 use `result` (which comes after mapped), but line 7 declares `result`
1781        // Let's extract lines 5-6 only: `filtered` and `mapped` are declared
1782        // and `filtered` is used on line 6, `mapped` is used on line 7
1783        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        // Enclosing function ends around line 10
1787        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        // `filtered` is declared in-range and used after the range
1793        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        // voidWork lines 20-21: no return, `greeting` is only used within
1802        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    // --- Function generation ---
1951
1952    #[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    // --- Python free variables ---
2052
2053    #[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        // process_data body: lines 5-8 reference `items` and `prefix`
2059        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    // --- validate_single_return ---
2077
2078    #[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(); // function_declaration
2085        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        // lexical_declaration > variable_declarator > arrow_function
2103        let lex_decl = root.child(0).unwrap();
2104        let var_decl = lex_decl.child(1).unwrap(); // variable_declarator
2105        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    // --- detect_scope_conflicts ---
2122
2123    #[test]
2124    fn scope_conflicts_none() {
2125        // No overlap between call site scope and body vars
2126        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        // `temp` exists at call site and inside body
2142        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    // --- substitute_params ---
2161
2162    #[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        // Should NOT replace `i` inside `items`
2175        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        // `items` should be untouched, and the nested arrow's shadowing `i`
2180        // parameter/reference pair should also be left alone.
2181        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}