Skip to main content

tsift_graph/
extract.rs

1//! Range-selected function extraction.
2//!
3//! Every other semantic edit selects a *named* thing — a symbol row, a heading,
4//! an ast-grep pattern — and rewrites at or around it. An extraction selects a
5//! run of sibling statements that has no name, no symbol row, and no single AST
6//! node, and produces two edits that must agree with each other: a new function
7//! whose signature is derived from the selection, and a call whose arguments
8//! come from the same derivation.
9//!
10//! That second property is why this module refuses so much. A rename that
11//! misses an occurrence breaks the build loudly. An extraction with a wrong
12//! parameter list can compile and silently change behaviour — a name that
13//! should have been a parameter falling through to a module-level binding of
14//! the same name is the exact case, which is why module scope is classified
15//! explicitly below rather than left to "not bound in the enclosing function".
16//!
17//! The derivation itself is language-general; what is not is the *vocabulary*
18//! it reads (which node kinds bind, read, block, and escape) and the *spelling*
19//! it emits. Those two live in [`Dialect`] and the emitters at the bottom of
20//! this file, so a language joins the untyped family by naming its node kinds
21//! rather than by growing a second copy of the analysis. The family is exactly
22//! the set of languages whose signature is derivable without type information:
23//! Python, GDScript, and the JS-like grammars. TypeScript is in it only because
24//! it can *copy* an annotation it already has — where it cannot, it refuses
25//! rather than writing `unknown` or an implicit `any`.
26
27use crate::lang::Lang;
28use std::collections::BTreeSet;
29use tree_sitter::{Node, Parser};
30
31/// A derived extraction, ready to be spelled by a language emitter.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ExtractionPlan {
34    /// The language the plan was derived from; the emitter reads it back so a
35    /// plan cannot be rendered with another language's spelling.
36    pub lang: Lang,
37    /// The function the range was taken out of.
38    pub enclosing_function: String,
39    /// Names read in the range before the range assigns them, that are bound in
40    /// the enclosing function outside the range. Sorted, so the signature and
41    /// the call site cannot disagree about argument order.
42    pub parameters: Vec<String>,
43    /// How each parameter is spelled in the new signature, positionally paired
44    /// with `parameters`. Identical to `parameters` in the untyped languages;
45    /// in TypeScript each entry carries the annotation copied from the name's
46    /// existing binding, because a parameter list is the one place a derived
47    /// signature cannot stay silent about types.
48    pub parameter_spellings: Vec<String>,
49    /// Names assigned in the range and read after it, in the same order rule.
50    pub returns: Vec<String>,
51    /// The new function's declared return type, where the language requires
52    /// one. `None` everywhere the return is inferred.
53    pub return_type: Option<String>,
54    /// Whether a call site that declares what it receives has to declare it
55    /// mutable, because something after the range assigns it.
56    pub returns_declared_mut: bool,
57    /// Names the range only *assigns*, whose declaration stayed behind in the
58    /// enclosing function. In a language where a bare assignment does not
59    /// declare, the new function has to declare them itself or its body reads a
60    /// name that is not there. Empty in Python, where assignment declares.
61    pub local_declarations: Vec<String>,
62    /// Whether the call site must *declare* the returned names rather than
63    /// assign them: true when the range carried their declaration away with it.
64    /// Always false where declarations do not exist (Python).
65    pub returns_need_declaration: bool,
66    /// Byte range of the statements being hoisted.
67    pub start_byte: usize,
68    pub end_byte: usize,
69    /// Indentation of the hoisted statements, so the emitter can re-indent the
70    /// body and place the call at the same depth.
71    pub indent: String,
72    /// Byte offset where the new function is inserted: immediately after the
73    /// enclosing function, at its own indentation.
74    pub insert_byte: usize,
75    /// Indentation of the enclosing function's own declaration.
76    pub enclosing_indent: String,
77    /// One level of indentation as this file actually writes it, measured from
78    /// the enclosing function's own body rather than assumed. A file indented
79    /// with tabs or two spaces gets a new function indented the same way.
80    pub indent_unit: String,
81}
82
83/// Why an extraction was refused.
84///
85/// Each variant names one invariant. A refusal is a first-class result here for
86/// the same reason it is in `edit-intents`: an extraction that guesses is worse
87/// than one that declines.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ExtractionRefusal {
90    /// The language has no extraction emitter yet.
91    UnsupportedLanguage(&'static str),
92    /// The source could not be parsed at all.
93    ParseFailed,
94    /// No statement starts inside the requested line range.
95    EmptyRange,
96    /// The selected statements do not share one block, so they are not a
97    /// contiguous run of siblings.
98    NotContiguousSiblings,
99    /// The range is not inside a function body.
100    NotInsideFunction,
101    /// The enclosing function is not itself a statement — it is a method, or a
102    /// function expression bound into a larger expression — so there is nowhere
103    /// beside it to put a new function without changing what the new function
104    /// is.
105    EnclosingFunctionNotHoistable,
106    /// Control flow leaves the range: hoisting it changes what it does, and no
107    /// signature can carry that.
108    ControlFlowEscapes(&'static str),
109    /// The range assigns a name the enclosing function declared `global` or
110    /// `nonlocal`; the assignment's effect is outside the new function's scope.
111    RebindsOuterScope(String),
112    /// The extracted name already binds something visible at the call site.
113    NameCollision(String),
114    /// The extraction would have to return several values in a language with no
115    /// spelling for that which keeps the call site one statement.
116    MultipleReturnsUnsupported(&'static str),
117    /// Some returned names were declared inside the range and others already
118    /// existed outside it. One call site cannot both declare and assign, and
119    /// splitting it into two statements would change what a caller reads.
120    MixedReturnDeclarations,
121    /// A name the range needs would have to be *moved* into the new function
122    /// and is still read afterwards. Passing it by reference instead would
123    /// mean rewriting every use in the body into a dereference, which is a
124    /// body rewrite this intent does not do.
125    MovedNameUsedAfterRange(String),
126    /// The range covers the block's trailing expression — the value the
127    /// enclosing function returns. Hoisting it would hand that value to the
128    /// new function and leave the caller returning nothing.
129    ReturnsThroughTailExpression,
130    /// The range names the receiver the enclosing function was called on.
131    /// Unlike Python's `self`, `this` is not a name a derived signature can
132    /// carry, and it means something different inside a plain function.
133    ReferencesReceiver(&'static str),
134    /// The range assigns a name it does not declare, and that name is not a
135    /// local of the enclosing function either. Declaring it inside the new
136    /// function would shadow an outer binding or turn a global into a local;
137    /// leaving it undeclared would write to a scope the caller did not mean.
138    AssignsUndeclaredName(String),
139    /// A parameter's type cannot be copied from an existing annotation, and
140    /// this language requires one. Writing `unknown` — or leaving it implicitly
141    /// `any` — would produce a signature that type-checks and means nothing.
142    UnspellableParameterType(String),
143}
144
145impl ExtractionRefusal {
146    /// A one-line message naming the failed invariant.
147    pub fn message(&self) -> String {
148        match self {
149            Self::UnsupportedLanguage(lang) => {
150                format!("extract_function has no emitter for {lang} yet")
151            }
152            Self::ParseFailed => "source could not be parsed".to_string(),
153            Self::EmptyRange => "no statement starts inside the requested line range".to_string(),
154            Self::NotContiguousSiblings => {
155                "the selected lines are not a contiguous run of sibling statements in one block"
156                    .to_string()
157            }
158            Self::NotInsideFunction => "the selected range is not inside a function".to_string(),
159            Self::EnclosingFunctionNotHoistable => {
160                "the enclosing function is a method or an expression, so a new function cannot be placed beside it"
161                    .to_string()
162            }
163            Self::ControlFlowEscapes(kind) => {
164                format!("the range contains `{kind}`, whose effect leaves the extracted function")
165            }
166            Self::RebindsOuterScope(name) => {
167                format!("the range assigns `{name}`, which the enclosing function declares global or nonlocal")
168            }
169            Self::NameCollision(name) => {
170                format!("`{name}` already binds a value visible at the call site")
171            }
172            Self::MultipleReturnsUnsupported(lang) => {
173                format!("the range produces several values and {lang} has no destructuring call site to receive them")
174            }
175            Self::MixedReturnDeclarations => {
176                "the range produces both newly declared and already declared names, which one call site cannot receive"
177                    .to_string()
178            }
179            Self::MovedNameUsedAfterRange(name) => {
180                format!("`{name}` would have to move into the extracted function and is still read after the range")
181            }
182            Self::ReturnsThroughTailExpression => {
183                "the range covers the trailing expression the enclosing function returns".to_string()
184            }
185            Self::ReferencesReceiver(keyword) => {
186                format!("the range uses `{keyword}`, which a derived signature cannot carry out of the method")
187            }
188            Self::AssignsUndeclaredName(name) => {
189                format!("the range assigns `{name}` without declaring it, and `{name}` is not a local of the enclosing function")
190            }
191            Self::UnspellableParameterType(name) => {
192                format!("`{name}` has no annotation to copy, and this language will not spell a type it cannot see")
193            }
194        }
195    }
196}
197
198/// The node-kind vocabulary and spelling rules of one extractable language.
199///
200/// Data, not prose: a grammar that renames `block` to `body` shows up as a
201/// changed row rather than as a comment that quietly stopped being true.
202#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203struct Dialect {
204    family: Family,
205    /// Whether the new signature has to carry parameter types.
206    annotates_parameters: bool,
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210enum Family {
211    /// `def`, indentation blocks, no declarations, tuple returns.
212    Python,
213    /// `func`, indentation blocks, `var` declarations, no destructuring.
214    GdScript,
215    /// `function`, brace blocks, `let` declarations, array destructuring.
216    JsLike,
217    /// `fn`, brace blocks, `let` declarations, tuple returns — and the only
218    /// member of the family whose signature carries ownership as well as a
219    /// type. See `rust_move_only` for why that keeps it by-value.
220    Rust,
221}
222
223fn dialect_for(lang: Lang) -> Option<Dialect> {
224    let dialect = match lang {
225        #[cfg(feature = "lang-python")]
226        Lang::Python => Dialect {
227            family: Family::Python,
228            annotates_parameters: false,
229        },
230        #[cfg(feature = "lang-gdscript")]
231        Lang::GdScript => Dialect {
232            family: Family::GdScript,
233            annotates_parameters: false,
234        },
235        #[cfg(feature = "lang-javascript")]
236        Lang::JavaScript | Lang::Jsx => Dialect {
237            family: Family::JsLike,
238            annotates_parameters: false,
239        },
240        #[cfg(feature = "lang-typescript")]
241        Lang::TypeScript | Lang::Tsx => Dialect {
242            family: Family::JsLike,
243            annotates_parameters: true,
244        },
245        #[cfg(feature = "lang-rust")]
246        Lang::Rust => Dialect {
247            family: Family::Rust,
248            annotates_parameters: true,
249        },
250        _ => return None,
251    };
252    Some(dialect)
253}
254
255impl Dialect {
256    fn root_kind(self) -> &'static str {
257        match self.family {
258            Family::Python => "module",
259            Family::GdScript => "source",
260            Family::JsLike => "program",
261            Family::Rust => "source_file",
262        }
263    }
264
265    fn is_block_kind(self, kind: &str) -> bool {
266        match self.family {
267            Family::Python => kind == "block",
268            Family::GdScript => kind == "body",
269            Family::JsLike => kind == "statement_block",
270            Family::Rust => kind == "block",
271        }
272    }
273
274    fn is_function_kind(self, kind: &str) -> bool {
275        match self.family {
276            Family::Python | Family::GdScript => kind == "function_definition",
277            Family::JsLike => matches!(
278                kind,
279                "function_declaration"
280                    | "generator_function_declaration"
281                    | "function_expression"
282                    | "function"
283                    | "generator_function"
284                    | "arrow_function"
285                    | "method_definition"
286            ),
287            // `closure_expression` is listed so a range inside a closure
288            // resolves to the closure rather than to the `fn` around it, and
289            // then refuses at the insertion site — hoisting past a closure
290            // would strand everything it captured.
291            Family::Rust => matches!(kind, "function_item" | "closure_expression"),
292        }
293    }
294
295    /// A scope that re-binds names of its own, so its body is not part of the
296    /// range's control flow or of the enclosing function's bindings.
297    fn is_nested_scope_kind(self, kind: &str) -> bool {
298        self.is_function_kind(kind)
299            || match self.family {
300                Family::Python => matches!(kind, "lambda" | "class_definition"),
301                Family::GdScript => matches!(kind, "lambda" | "class_definition"),
302                Family::JsLike => matches!(kind, "class_declaration" | "class"),
303                Family::Rust => matches!(
304                    kind,
305                    "impl_item" | "trait_item" | "struct_item" | "enum_item" | "mod_item"
306                ),
307            }
308    }
309
310    fn is_class_kind(self, kind: &str) -> bool {
311        match self.family {
312            Family::Python | Family::GdScript => kind == "class_definition",
313            Family::JsLike => matches!(kind, "class_declaration" | "class"),
314            Family::Rust => matches!(kind, "impl_item" | "trait_item"),
315        }
316    }
317
318    /// The node that holds a class's members, where it is spelled separately
319    /// from the class itself.
320    fn is_class_body_kind(self, kind: &str) -> bool {
321        match self.family {
322            // Python reuses its ordinary block node, so a Python class body is
323            // recognized through its parent instead.
324            Family::Python => false,
325            Family::GdScript | Family::JsLike => kind == "class_body",
326            Family::Rust => kind == "declaration_list",
327        }
328    }
329
330    /// Whether a new function extracted from a method belongs *outside* the
331    /// class rather than beside the method.
332    ///
333    /// Python and JavaScript resolve a bare call through the enclosing lexical
334    /// scope, never through the class, so the new function has to leave.
335    /// GDScript resolves a bare call against the script's own members, so it
336    /// has to stay.
337    fn hoists_out_of_class(self) -> bool {
338        !matches!(self.family, Family::GdScript)
339    }
340
341    /// Node kinds that name the receiver the enclosing function was called on.
342    ///
343    /// A `this` moved into a plain function stops meaning what it meant, and
344    /// unlike Python's `self` it is not a name a signature can carry.
345    fn receiver_kinds(self) -> &'static [&'static str] {
346        match self.family {
347            Family::Python | Family::GdScript => &[],
348            Family::JsLike => &["this", "super"],
349            Family::Rust => &["self"],
350        }
351    }
352
353    /// Kinds whose effect is defined by something outside themselves.
354    ///
355    /// `break` and `continue` are conditional — see `escaping_control_flow`,
356    /// which only counts them when the loop they bind to is outside the range.
357    fn escaping_kind(self, kind: &str) -> Option<&'static str> {
358        let escape = match (self.family, kind) {
359            (_, "return_statement") => "return",
360            (_, "break_statement") => "break",
361            (_, "continue_statement") => "continue",
362            (Family::Python, "yield") => "yield",
363            (Family::JsLike, "yield_expression") => "yield",
364            (Family::Rust, "return_expression") => "return",
365            (Family::Rust, "break_expression") => "break",
366            (Family::Rust, "continue_expression") => "continue",
367            // `?` returns from the *enclosing* function, and `.await` needs a
368            // context the new function's signature does not say it has.
369            (Family::Rust, "try_expression") => "?",
370            (Family::Rust, "await_expression") => ".await",
371            // `throw`/`raise` is deliberately absent: an exception propagates
372            // through a call frame unchanged, so hoisting it does not move
373            // where it is caught.
374            _ => return None,
375        };
376        Some(escape)
377    }
378
379    /// A construct `break` and `continue` bind to.
380    fn is_loop_kind(self, kind: &str) -> bool {
381        match self.family {
382            Family::Python | Family::GdScript => matches!(kind, "for_statement" | "while_statement"),
383            Family::JsLike => matches!(
384                kind,
385                "for_statement" | "for_in_statement" | "while_statement" | "do_statement"
386            ),
387            Family::Rust => {
388                matches!(kind, "for_expression" | "while_expression" | "loop_expression")
389            }
390        }
391    }
392
393    /// A construct `break` alone binds to.
394    fn is_switch_kind(self, kind: &str) -> bool {
395        matches!(self.family, Family::JsLike) && kind == "switch_statement"
396    }
397
398    /// Kinds that group several binding positions into one target.
399    fn pattern_kinds(self) -> &'static [&'static str] {
400        match self.family {
401            Family::Python => &["pattern_list", "tuple_pattern", "list_pattern"],
402            Family::GdScript => &[],
403            Family::Rust => &[
404                "tuple_pattern",
405                "tuple_struct_pattern",
406                "struct_pattern",
407                "slice_pattern",
408                "ref_pattern",
409                "mut_pattern",
410            ],
411            Family::JsLike => &[
412                "object_pattern",
413                "array_pattern",
414                "pair_pattern",
415                "rest_pattern",
416                "assignment_pattern",
417                "object_assignment_pattern",
418            ],
419        }
420    }
421
422    /// The keyword a call site uses to declare the names it receives, where the
423    /// language has one.
424    fn declaration_keyword(self) -> Option<&'static str> {
425        match self.family {
426            Family::Python => None,
427            Family::GdScript => Some("var"),
428            Family::JsLike | Family::Rust => Some("let"),
429        }
430    }
431
432    fn default_indent_unit(self) -> &'static str {
433        match self.family {
434            Family::Python | Family::Rust => "    ",
435            Family::GdScript => "\t",
436            Family::JsLike => "  ",
437        }
438    }
439
440    /// Whether a parameter is moved rather than borrowed, so a name handed in
441    /// is gone from the caller unless it is handed back.
442    fn moves_parameters(self) -> bool {
443        matches!(self.family, Family::Rust)
444    }
445
446    /// Whether the signature spells mutability as well as a type.
447    fn annotates_mutability(self) -> bool {
448        matches!(self.family, Family::Rust)
449    }
450
451    /// Whether the language requires the new function to declare its return
452    /// type rather than inferring it.
453    fn annotates_return_type(self) -> bool {
454        matches!(self.family, Family::Rust)
455    }
456
457    /// How many values a single call-site statement can receive.
458    fn max_returns(self) -> Option<usize> {
459        match self.family {
460            // GDScript has no multiple assignment and no destructuring, so more
461            // than one returned name cannot be received without splitting the
462            // call site into statements a caller did not ask for.
463            Family::GdScript => Some(1),
464            _ => None,
465        }
466    }
467}
468
469/// Derive an extraction for the statements covered by `start_line..=end_line`
470/// (zero-based, inclusive).
471pub fn plan_extraction(
472    lang: Lang,
473    source: &[u8],
474    start_line: usize,
475    end_line: usize,
476    new_name: &str,
477) -> Result<ExtractionPlan, ExtractionRefusal> {
478    let dialect =
479        dialect_for(lang).ok_or(ExtractionRefusal::UnsupportedLanguage(lang.name()))?;
480    let ts_lang = lang.tree_sitter_language();
481    let mut parser = Parser::new();
482    parser
483        .set_language(&ts_lang)
484        .map_err(|_| ExtractionRefusal::ParseFailed)?;
485    let tree = parser
486        .parse(source, None)
487        .ok_or(ExtractionRefusal::ParseFailed)?;
488    let root = tree.root_node();
489
490    let selection = select_sibling_run(dialect, root, start_line, end_line)?;
491    let block = selection[0]
492        .parent()
493        .ok_or(ExtractionRefusal::NotContiguousSiblings)?;
494    let function = enclosing_function(dialect, block)?;
495    let insertion_site = insertion_site(dialect, function)?;
496
497    for statement in &selection {
498        if is_tail_expression(dialect, *statement) {
499            return Err(ExtractionRefusal::ReturnsThroughTailExpression);
500        }
501        if let Some(kind) = escaping_control_flow(dialect, *statement, source) {
502            return Err(ExtractionRefusal::ControlFlowEscapes(kind));
503        }
504        if let Some(kind) = receiver_reference(dialect, *statement) {
505            return Err(ExtractionRefusal::ReferencesReceiver(kind));
506        }
507    }
508
509    let start_byte = selection[0].start_byte();
510    let end_byte = selection[selection.len() - 1].end_byte();
511
512    let bindings = range_bindings(dialect, &selection, source);
513    let assigned_in_range = bindings.all();
514    let scope_pinned = scope_pinned_names(dialect, function, source);
515    if let Some(name) = assigned_in_range.intersection(&scope_pinned).next() {
516        return Err(ExtractionRefusal::RebindsOuterScope(name.clone()));
517    }
518
519    let read_first_in_range = names_read_before_assignment(dialect, &selection, source);
520    let bound_outside_range =
521        names_bound_in_function_outside(dialect, function, start_byte, end_byte, source);
522    let read_after_range = names_read_after(dialect, function, end_byte, source);
523    let module_scope = scope_bindings(dialect, root, source);
524    // Where the new function actually lands, which is not module scope once it
525    // has climbed out of a class — or into a GDScript class alongside methods
526    // the root walk never sees.
527    let sibling_scope = insertion_site
528        .parent()
529        .map(|parent| scope_bindings(dialect, parent, source))
530        .unwrap_or_default();
531
532    if bound_outside_range.contains(new_name)
533        || module_scope.contains(new_name)
534        || sibling_scope.contains(new_name)
535    {
536        return Err(ExtractionRefusal::NameCollision(new_name.to_string()));
537    }
538
539    let mut parameters = read_first_in_range
540        .intersection(&bound_outside_range)
541        .cloned()
542        .collect::<Vec<_>>();
543    // A receiver threaded out of a method reads as the first argument
544    // everywhere else in the language; alphabetical order would put it in the
545    // middle and make a correct signature look wrong.
546    if let Some(position) = parameters.iter().position(|name| name == "self") {
547        let receiver = parameters.remove(position);
548        parameters.insert(0, receiver);
549    }
550    let returns = assigned_in_range
551        .intersection(&read_after_range)
552        .cloned()
553        .collect::<Vec<_>>();
554
555    if let Some(limit) = dialect.max_returns()
556        && returns.len() > limit
557    {
558        return Err(ExtractionRefusal::MultipleReturnsUnsupported(lang.name()));
559    }
560
561    let returns_need_declaration = resolve_return_declaration(
562        dialect,
563        &returns,
564        &bindings.declared,
565        &bound_outside_range,
566    )?;
567    let local_declarations =
568        resolve_local_declarations(dialect, &bindings, &parameters, &bound_outside_range)?;
569
570    // Rust passes every parameter by value, so a name that is moved in and not
571    // handed back cannot still be read afterwards. Passing it by reference
572    // would compile only after rewriting each use in the body into a
573    // dereference, and rewriting bodies is the one thing this intent does not
574    // do — so it refuses and says which name forced it.
575    if dialect.moves_parameters() {
576        for parameter in &parameters {
577            if read_after_range.contains(parameter) && !returns.contains(parameter) {
578                return Err(ExtractionRefusal::MovedNameUsedAfterRange(parameter.clone()));
579            }
580        }
581    }
582
583    let mut parameter_spellings = Vec::with_capacity(parameters.len());
584    for parameter in &parameters {
585        let mutable = dialect.annotates_mutability() && bindings.all().contains(parameter);
586        parameter_spellings.push(spell_parameter(
587            dialect, function, parameter, mutable, source,
588        )?);
589    }
590    let return_type = spell_return_type(dialect, function, &returns, source)?;
591    let returns_declared_mut = returns_need_declaration
592        && returns
593            .iter()
594            .any(|name| names_assigned_after(dialect, function, end_byte, source).contains(name));
595
596    Ok(ExtractionPlan {
597        lang,
598        enclosing_function: function
599            .child_by_field_name("name")
600            .and_then(|name| name.utf8_text(source).ok())
601            .unwrap_or_default()
602            .to_string(),
603        parameters,
604        parameter_spellings,
605        returns,
606        return_type,
607        returns_declared_mut,
608        local_declarations,
609        returns_need_declaration,
610        start_byte,
611        end_byte,
612        indent: line_indent(source, start_byte),
613        insert_byte: insertion_site.end_byte(),
614        enclosing_indent: line_indent(source, insertion_site.start_byte()),
615        indent_unit: indent_unit(dialect, function, source),
616    })
617}
618
619/// Render an extraction: the new function and the call that replaces the range,
620/// both already indented for their positions.
621pub fn render_extraction(plan: &ExtractionPlan, source: &str, new_name: &str) -> (String, String) {
622    let dialect = dialect_for(plan.lang).expect("a plan is only built for an extractable language");
623    let inner_indent = format!("{}{}", plan.enclosing_indent, plan.indent_unit);
624    let body = format!(
625        "{}{}",
626        local_declaration_prologue(&dialect, plan, &inner_indent),
627        reindent_body(plan, source, &inner_indent)
628    );
629    let signature = plan.parameter_spellings.join(", ");
630    let arguments = plan.parameters.join(", ");
631    let call_expression = format!("{new_name}({arguments})");
632
633    match dialect.family {
634        Family::Python => {
635            let mut function = format!(
636                "\n\n{}def {new_name}({signature}):\n{body}",
637                plan.enclosing_indent
638            );
639            if !plan.returns.is_empty() {
640                function.push('\n');
641                function.push_str(&inner_indent);
642                function.push_str("return ");
643                function.push_str(&plan.returns.join(", "));
644            }
645            function.push('\n');
646            let call = if plan.returns.is_empty() {
647                format!("{}{call_expression}", plan.indent)
648            } else {
649                format!(
650                    "{}{} = {call_expression}",
651                    plan.indent,
652                    plan.returns.join(", ")
653                )
654            };
655            (function, call)
656        }
657        Family::GdScript => {
658            let mut function = format!(
659                "\n\n{}func {new_name}({signature}):\n{body}",
660                plan.enclosing_indent
661            );
662            if let Some(returned) = plan.returns.first() {
663                function.push('\n');
664                function.push_str(&inner_indent);
665                function.push_str("return ");
666                function.push_str(returned);
667            }
668            function.push('\n');
669            let call = match plan.returns.first() {
670                None => format!("{}{call_expression}", plan.indent),
671                Some(returned) => format!(
672                    "{}{}{returned} = {call_expression}",
673                    plan.indent,
674                    declaration_prefix(&dialect, plan)
675                ),
676            };
677            (function, call)
678        }
679        Family::JsLike => {
680            let mut function = format!(
681                "\n\n{}function {new_name}({signature}) {{\n{body}",
682                plan.enclosing_indent
683            );
684            if !plan.returns.is_empty() {
685                function.push('\n');
686                function.push_str(&inner_indent);
687                function.push_str("return ");
688                function.push_str(&js_return_target(&plan.returns));
689                function.push(';');
690            }
691            function.push('\n');
692            function.push_str(&plan.enclosing_indent);
693            function.push_str("}\n");
694            let call = if plan.returns.is_empty() {
695                format!("{}{call_expression};", plan.indent)
696            } else {
697                format!(
698                    "{}{}{} = {call_expression};",
699                    plan.indent,
700                    declaration_prefix(&dialect, plan),
701                    js_return_target(&plan.returns)
702                )
703            };
704            (function, call)
705        }
706        Family::Rust => {
707            let returns = match &plan.return_type {
708                Some(spelled) => format!(" -> {spelled}"),
709                None => String::new(),
710            };
711            let mut function = format!(
712                "\n\n{}fn {new_name}({signature}){returns} {{\n{body}",
713                plan.enclosing_indent
714            );
715            if !plan.returns.is_empty() {
716                // A trailing expression, not `return`: the idiom the language
717                // reads as a value handed back rather than a jump.
718                function.push('\n');
719                function.push_str(&inner_indent);
720                function.push_str(&rust_return_target(&plan.returns));
721            }
722            function.push('\n');
723            function.push_str(&plan.enclosing_indent);
724            function.push_str("}\n");
725            let call = if plan.returns.is_empty() {
726                format!("{}{call_expression};", plan.indent)
727            } else {
728                format!(
729                    "{}{}{} = {call_expression};",
730                    plan.indent,
731                    declaration_prefix(&dialect, plan),
732                    rust_return_target(&plan.returns)
733                )
734            };
735            (function, call)
736        }
737    }
738}
739
740/// One returned name is itself; several are a tuple, which the call site
741/// destructures in the same shape.
742fn rust_return_target(returns: &[String]) -> String {
743    if returns.len() == 1 {
744        returns[0].clone()
745    } else {
746        format!("({})", returns.join(", "))
747    }
748}
749
750/// The declarations the new function opens with, for names the range assigns
751/// but whose declaration stayed behind in the enclosing function.
752fn local_declaration_prologue(
753    dialect: &Dialect,
754    plan: &ExtractionPlan,
755    inner_indent: &str,
756) -> String {
757    let Some(keyword) = dialect.declaration_keyword() else {
758        return String::new();
759    };
760    let terminator = if matches!(dialect.family, Family::JsLike | Family::Rust) {
761        ";"
762    } else {
763        ""
764    };
765    plan.local_declarations
766        .iter()
767        .map(|name| format!("{inner_indent}{keyword} {name}{terminator}\n"))
768        .collect()
769}
770
771/// `let ` / `var ` when the range carried the declaration away, empty when the
772/// names still exist at the call site.
773fn declaration_prefix(dialect: &Dialect, plan: &ExtractionPlan) -> String {
774    if !plan.returns_need_declaration {
775        return String::new();
776    }
777    let Some(keyword) = dialect.declaration_keyword() else {
778        return String::new();
779    };
780    // Only where the binding spells mutability, and only when something after
781    // the range assigns it — an unconditional `mut` would compile and warn.
782    let mutable = if dialect.annotates_mutability() && plan.returns_declared_mut {
783        "mut "
784    } else {
785        ""
786    };
787    format!("{keyword} {mutable}")
788}
789
790/// One returned name is itself; several are an array, which is the only
791/// spelling that keeps the JS call site a single statement.
792fn js_return_target(returns: &[String]) -> String {
793    if returns.len() == 1 {
794        returns[0].clone()
795    } else {
796        format!("[{}]", returns.join(", "))
797    }
798}
799
800/// The hoisted statements, re-indented for the new function's body while
801/// keeping their relative nesting.
802fn reindent_body(plan: &ExtractionPlan, source: &str, inner_indent: &str) -> String {
803    let body = &source[plan.start_byte..plan.end_byte];
804    let mut rendered = String::new();
805    for (index, line) in body.lines().enumerate() {
806        if index > 0 {
807            rendered.push('\n');
808        }
809        if line.trim().is_empty() {
810            continue;
811        }
812        let stripped = line.strip_prefix(&plan.indent).unwrap_or(line);
813        rendered.push_str(inner_indent);
814        rendered.push_str(stripped);
815    }
816    rendered
817}
818
819/// Whether the call site declares the names it receives.
820///
821/// A name needs declaring only when the range carried its *declaration* away
822/// and nothing outside the range binds it. Reading declarations rather than
823/// bindings is what keeps an implicit global — assigned in the range, never
824/// declared anywhere — from being turned into a local by the call site.
825///
826/// Mixed is a refusal rather than a guess: a call site that declared the new
827/// names and assigned the old ones would need two statements, and the second
828/// would read a binding the first had just shadowed.
829fn resolve_return_declaration(
830    dialect: Dialect,
831    returns: &[String],
832    declared_in_range: &BTreeSet<String>,
833    bound_outside_range: &BTreeSet<String>,
834) -> Result<bool, ExtractionRefusal> {
835    if dialect.declaration_keyword().is_none() || returns.is_empty() {
836        return Ok(false);
837    }
838    let new_names = returns
839        .iter()
840        .filter(|name| {
841            declared_in_range.contains(*name) && !bound_outside_range.contains(*name)
842        })
843        .count();
844    if new_names == 0 {
845        return Ok(false);
846    }
847    if new_names == returns.len() {
848        return Ok(true);
849    }
850    Err(ExtractionRefusal::MixedReturnDeclarations)
851}
852
853/// How one parameter is written in the new signature.
854///
855/// Only TypeScript needs more than the name, and it gets it by *copying* an
856/// annotation the file already has rather than inventing one.
857fn spell_parameter(
858    dialect: Dialect,
859    function: Node,
860    name: &str,
861    mutable: bool,
862    source: &[u8],
863) -> Result<String, ExtractionRefusal> {
864    if !dialect.annotates_parameters {
865        return Ok(name.to_string());
866    }
867    let annotation = existing_type_annotation(dialect, function, name, source)
868        .ok_or_else(|| ExtractionRefusal::UnspellableParameterType(name.to_string()))?;
869    Ok(match dialect.family {
870        // TypeScript's annotation node carries its own `: `.
871        Family::Rust => {
872            let prefix = if mutable { "mut " } else { "" };
873            format!("{prefix}{name}: {annotation}")
874        }
875        _ => format!("{name}{annotation}"),
876    })
877}
878
879/// The type the new function declares it returns, where the language makes it
880/// say so. Several returns are a tuple, which is also the shape the call site
881/// destructures.
882fn spell_return_type(
883    dialect: Dialect,
884    function: Node,
885    returns: &[String],
886    source: &[u8],
887) -> Result<Option<String>, ExtractionRefusal> {
888    if !dialect.annotates_return_type() || returns.is_empty() {
889        return Ok(None);
890    }
891    let mut spelled = Vec::with_capacity(returns.len());
892    for name in returns {
893        spelled.push(
894            existing_type_annotation(dialect, function, name, source)
895                .ok_or_else(|| ExtractionRefusal::UnspellableParameterType(name.clone()))?,
896        );
897    }
898    Ok(Some(if spelled.len() == 1 {
899        spelled.remove(0)
900    } else {
901        format!("({})", spelled.join(", "))
902    }))
903}
904
905/// The annotation text (`": number"`) attached to `name`'s binding inside the
906/// enclosing function, if it has one.
907fn existing_type_annotation(
908    dialect: Dialect,
909    function: Node,
910    name: &str,
911    source: &[u8],
912) -> Option<String> {
913    let mut found = None;
914    walk(function, &mut |node| {
915        if found.is_some() {
916            return false;
917        }
918        let binder = match (dialect.family, node.kind()) {
919            (Family::JsLike, "required_parameter" | "optional_parameter") => {
920                node.child_by_field_name("pattern")
921            }
922            (Family::JsLike, "variable_declarator") => node.child_by_field_name("name"),
923            (Family::Rust, "parameter" | "let_declaration") => {
924                node.child_by_field_name("pattern")
925            }
926            _ => None,
927        };
928        if let Some(binder) = binder
929            && binder.kind() == "identifier"
930            && binder.utf8_text(source).is_ok_and(|text| text == name)
931            && let Some(annotation) = node.child_by_field_name("type")
932            && let Ok(text) = annotation.utf8_text(source)
933        {
934            found = Some(text.to_string());
935            return false;
936        }
937        true
938    });
939    found
940}
941
942/// One level of indentation as the enclosing function's own body writes it.
943fn indent_unit(dialect: Dialect, function: Node, source: &[u8]) -> String {
944    let enclosing_indent = line_indent(source, function.start_byte());
945    let measured = function
946        .child_by_field_name("body")
947        .and_then(|body| body.named_child(0))
948        .map(|statement| line_indent(source, statement.start_byte()))
949        .and_then(|body_indent| {
950            body_indent
951                .strip_prefix(&enclosing_indent)
952                .map(str::to_string)
953        })
954        .filter(|unit| !unit.is_empty());
955    measured.unwrap_or_else(|| dialect.default_indent_unit().to_string())
956}
957
958/// The statements that start inside the requested lines, verified to be a
959/// contiguous run of siblings in one block.
960fn select_sibling_run(
961    dialect: Dialect,
962    root: Node,
963    start_line: usize,
964    end_line: usize,
965) -> Result<Vec<Node>, ExtractionRefusal> {
966    let mut selected: Vec<Node> = Vec::new();
967    let mut cursor = root.walk();
968    let mut descend = true;
969    loop {
970        if descend {
971            let node = cursor.node();
972            let row = node.start_position().row;
973            if node.is_named()
974                && row >= start_line
975                && row <= end_line
976                && node.parent().is_some_and(|parent| {
977                    dialect.is_block_kind(parent.kind()) || parent.kind() == dialect.root_kind()
978                })
979            {
980                selected.push(node);
981                // A statement's children cannot also be top-level statements of
982                // the same run, so the walk does not descend into a match.
983                if cursor.goto_next_sibling() {
984                    continue;
985                }
986                if !cursor.goto_parent() {
987                    break;
988                }
989                descend = false;
990                continue;
991            }
992            if cursor.goto_first_child() {
993                continue;
994            }
995        }
996        if cursor.goto_next_sibling() {
997            descend = true;
998            continue;
999        }
1000        if !cursor.goto_parent() {
1001            break;
1002        }
1003        descend = false;
1004    }
1005
1006    if selected.is_empty() {
1007        return Err(ExtractionRefusal::EmptyRange);
1008    }
1009    let first_parent = selected[0].parent().map(|parent| parent.id());
1010    if selected
1011        .iter()
1012        .any(|node| node.parent().map(|parent| parent.id()) != first_parent)
1013    {
1014        return Err(ExtractionRefusal::NotContiguousSiblings);
1015    }
1016    // Siblings in source order with nothing named between them.
1017    for pair in selected.windows(2) {
1018        if pair[0].next_named_sibling().map(|next| next.id()) != Some(pair[1].id()) {
1019            return Err(ExtractionRefusal::NotContiguousSiblings);
1020        }
1021    }
1022    Ok(selected)
1023}
1024
1025/// The enclosing function, verified to be a statement something can be placed
1026/// beside.
1027///
1028/// A method or a function expression fails here rather than later: hoisting out
1029/// of a method would emit a sibling method, and the bare call left behind would
1030/// not resolve to it — code that parses, formats, and does not run.
1031fn enclosing_function(dialect: Dialect, block: Node) -> Result<Node, ExtractionRefusal> {
1032    let mut current = Some(block);
1033    while let Some(candidate) = current {
1034        if dialect.is_function_kind(candidate.kind()) {
1035            return Ok(candidate);
1036        }
1037        current = candidate.parent();
1038    }
1039    Err(ExtractionRefusal::NotInsideFunction)
1040}
1041
1042/// The construct the new function is placed after.
1043///
1044/// Usually the enclosing function itself. Inside a method it is the *class*,
1045/// because a `def` placed beside a method is another method and the bare call
1046/// left behind does not resolve to it — so the extraction climbs out to where
1047/// the call can see it. Climbing past a class body never costs the extracted
1048/// body anything: a method could not read a class-body name unqualified in the
1049/// first place, so nothing it closed over is left behind.
1050///
1051/// GDScript is the exception, and for the opposite reason: its methods *do*
1052/// call each other bare, so a sibling `func` in the same class is exactly
1053/// right and climbing out would break the call instead of fixing it.
1054fn insertion_site(dialect: Dialect, function: Node) -> Result<Node, ExtractionRefusal> {
1055    let mut node = function;
1056    loop {
1057        let Some(parent) = node.parent() else {
1058            return Err(ExtractionRefusal::EnclosingFunctionNotHoistable);
1059        };
1060        // A class body holds declarations the same way a block holds
1061        // statements, so both are places something can be put; what differs is
1062        // whether staying there keeps the call resolvable.
1063        let in_class_body = dialect.is_class_body_kind(parent.kind())
1064            || (dialect.is_block_kind(parent.kind())
1065                && parent
1066                    .parent()
1067                    .is_some_and(|grand| dialect.is_class_kind(grand.kind())));
1068        if in_class_body {
1069            if !dialect.hoists_out_of_class() {
1070                return Ok(node);
1071            }
1072            let Some(class) = parent.parent() else {
1073                return Err(ExtractionRefusal::EnclosingFunctionNotHoistable);
1074            };
1075            node = class;
1076            continue;
1077        }
1078        if dialect.is_block_kind(parent.kind()) || parent.kind() == dialect.root_kind() {
1079            return Ok(node);
1080        }
1081        // Everything else — an arrow function, a function expression, a class
1082        // expression — is part of a larger expression, and there is no
1083        // statement slot beside it to put anything in.
1084        return Err(ExtractionRefusal::EnclosingFunctionNotHoistable);
1085    }
1086}
1087
1088/// Control flow whose effect is defined outside the range, and which therefore
1089/// cannot move into another function.
1090///
1091/// `return` and `yield` always qualify: no signature can carry them. `break`
1092/// and `continue` only qualify when the loop they bind to is *outside* the
1093/// selection — a range containing a whole loop takes that loop's `break` with
1094/// it, and refusing there would decline the most ordinary extraction there is.
1095/// A labelled branch is checked against the labels the range itself carries.
1096fn escaping_control_flow(
1097    dialect: Dialect,
1098    statement: Node,
1099    source: &[u8],
1100) -> Option<&'static str> {
1101    let mut labels = Vec::new();
1102    scan_control_flow(dialect, statement, source, true, 0, 0, &mut labels)
1103}
1104
1105fn scan_control_flow(
1106    dialect: Dialect,
1107    node: Node,
1108    source: &[u8],
1109    is_root: bool,
1110    loops: usize,
1111    switches: usize,
1112    labels: &mut Vec<String>,
1113) -> Option<&'static str> {
1114    // A nested function or lambda re-scopes these, so its body is not part of
1115    // the range's control flow.
1116    if !is_root && dialect.is_nested_scope_kind(node.kind()) {
1117        return None;
1118    }
1119    match dialect.escaping_kind(node.kind()) {
1120        Some(escape @ ("break" | "continue")) => {
1121            let bound_here = match escape {
1122                "break" => loops > 0 || switches > 0,
1123                _ => loops > 0,
1124            };
1125            return match branch_label(node, source) {
1126                // A labelled branch ignores the innermost loop and jumps to
1127                // the label, so what matters is whether the label is inside
1128                // the range.
1129                Some(label) if !labels.contains(&label) => Some(escape),
1130                Some(_) => None,
1131                None if bound_here => None,
1132                None => Some(escape),
1133            };
1134        }
1135        Some(escape) => return Some(escape),
1136        None => {}
1137    }
1138
1139    let loops = loops + usize::from(dialect.is_loop_kind(node.kind()));
1140    let switches = switches + usize::from(dialect.is_switch_kind(node.kind()));
1141    let pushed = label_name(node, source).inspect(|label| labels.push(label.clone()));
1142
1143    let mut found = None;
1144    let mut cursor = node.walk();
1145    for child in node.named_children(&mut cursor) {
1146        found = scan_control_flow(dialect, child, source, false, loops, switches, labels);
1147        if found.is_some() {
1148            break;
1149        }
1150    }
1151    if pushed.is_some() {
1152        labels.pop();
1153    }
1154    found
1155}
1156
1157/// The label a `break`/`continue` names, where the language has them.
1158fn branch_label(node: Node, source: &[u8]) -> Option<String> {
1159    node.child_by_field_name("label")
1160        .and_then(|label| label.utf8_text(source).ok())
1161        .map(str::to_string)
1162}
1163
1164/// The label this statement defines, if it defines one.
1165fn label_name(node: Node, source: &[u8]) -> Option<String> {
1166    if node.kind() != "labeled_statement" {
1167        return None;
1168    }
1169    branch_label(node, source)
1170}
1171
1172/// Whether this node is the block's trailing expression rather than a
1173/// statement — the value its function hands back.
1174///
1175/// Only Rust has one. It is recognized structurally: a block's children are
1176/// statements plus an optional final expression, so a last child that is not a
1177/// statement kind is that expression.
1178fn is_tail_expression(dialect: Dialect, node: Node) -> bool {
1179    if !matches!(dialect.family, Family::Rust) {
1180        return false;
1181    }
1182    if node.next_named_sibling().is_some() {
1183        return false;
1184    }
1185    if !node
1186        .parent()
1187        .is_some_and(|parent| dialect.is_block_kind(parent.kind()))
1188    {
1189        return false;
1190    }
1191    !matches!(node.kind(), "expression_statement" | "let_declaration")
1192        && !node.kind().ends_with("_item")
1193        && node.kind() != "attribute_item"
1194        && node.kind() != "macro_invocation"
1195}
1196
1197/// Names something after the range assigns, so a call site that declares what
1198/// it receives knows whether to declare it mutable.
1199fn names_assigned_after(
1200    dialect: Dialect,
1201    function: Node,
1202    end_byte: usize,
1203    source: &[u8],
1204) -> BTreeSet<String> {
1205    let mut names = BTreeSet::new();
1206    walk(function, &mut |node| {
1207        if node.end_byte() <= end_byte {
1208            return false;
1209        }
1210        if node.start_byte() >= end_byte
1211            && node
1212                .parent()
1213                .is_some_and(|parent| is_assignment_kind(dialect, parent.kind()))
1214            && let Some(name) = binding_name(dialect, node, source)
1215        {
1216            names.insert(name);
1217        }
1218        true
1219    });
1220    names
1221}
1222
1223/// The receiver keyword the range names, if it names one.
1224fn receiver_reference(dialect: Dialect, statement: Node) -> Option<&'static str> {
1225    let keywords = dialect.receiver_kinds();
1226    if keywords.is_empty() {
1227        return None;
1228    }
1229    let mut found = None;
1230    walk(statement, &mut |node| {
1231        if found.is_some() {
1232            return false;
1233        }
1234        found = keywords
1235            .iter()
1236            .find(|keyword| **keyword == node.kind())
1237            .copied();
1238        found.is_none()
1239    });
1240    found
1241}
1242
1243/// Names the enclosing function pinned to an outer scope.
1244fn scope_pinned_names(dialect: Dialect, function: Node, source: &[u8]) -> BTreeSet<String> {
1245    let mut names = BTreeSet::new();
1246    if dialect.family != Family::Python {
1247        return names;
1248    }
1249    walk(function, &mut |node| {
1250        if matches!(node.kind(), "global_statement" | "nonlocal_statement") {
1251            let mut cursor = node.walk();
1252            for child in node.named_children(&mut cursor) {
1253                if child.kind() == "identifier"
1254                    && let Ok(text) = child.utf8_text(source)
1255                {
1256                    names.insert(text.to_string());
1257                }
1258            }
1259        }
1260        true
1261    });
1262    names
1263}
1264
1265/// What the range does to each name it binds.
1266///
1267/// The split matters only in languages where a bare assignment does not
1268/// declare: a `let` that moved into the new function takes its declaration
1269/// with it, while an assignment to a name declared behind leaves the new
1270/// function reading something that is not there.
1271struct RangeBindings {
1272    /// Names bound by a construct that declares: a declaration statement, a
1273    /// loop head, a catch parameter, a nested function or class.
1274    declared: BTreeSet<String>,
1275    /// Names bound only by a plain or augmented assignment.
1276    assigned: BTreeSet<String>,
1277}
1278
1279impl RangeBindings {
1280    fn all(&self) -> BTreeSet<String> {
1281        self.declared.union(&self.assigned).cloned().collect()
1282    }
1283}
1284
1285fn range_bindings(dialect: Dialect, statements: &[Node], source: &[u8]) -> RangeBindings {
1286    let mut declared = BTreeSet::new();
1287    let mut assigned = BTreeSet::new();
1288    for statement in statements {
1289        walk(*statement, &mut |node| {
1290            if let Some(name) = binding_name(dialect, node, source) {
1291                if node
1292                    .parent()
1293                    .is_some_and(|parent| is_assignment_kind(dialect, parent.kind()))
1294                {
1295                    assigned.insert(name);
1296                } else {
1297                    declared.insert(name);
1298                }
1299            }
1300            true
1301        });
1302    }
1303    // A name the range both declares and assigns is declared: the declaration
1304    // moved with the statements.
1305    assigned.retain(|name| !declared.contains(name));
1306    RangeBindings { declared, assigned }
1307}
1308
1309/// The names the new function has to declare for its own body to make sense.
1310///
1311/// Only a name the range *assigns* without declaring needs one, and only where
1312/// a bare assignment does not declare. A name that is not a local of the
1313/// enclosing function refuses instead: declaring it would shadow an outer
1314/// binding, and not declaring it would write to a scope the caller did not
1315/// choose. Python is exempt by construction — there, assignment declares.
1316fn resolve_local_declarations(
1317    dialect: Dialect,
1318    bindings: &RangeBindings,
1319    parameters: &[String],
1320    bound_outside_range: &BTreeSet<String>,
1321) -> Result<Vec<String>, ExtractionRefusal> {
1322    if dialect.declaration_keyword().is_none() {
1323        return Ok(Vec::new());
1324    }
1325    let mut locals = Vec::new();
1326    for name in &bindings.assigned {
1327        // A parameter is already declared by the signature.
1328        if parameters.iter().any(|parameter| parameter == name) {
1329            continue;
1330        }
1331        if !bound_outside_range.contains(name) {
1332            return Err(ExtractionRefusal::AssignsUndeclaredName(name.clone()));
1333        }
1334        locals.push(name.clone());
1335    }
1336    Ok(locals)
1337}
1338
1339fn is_assignment_kind(dialect: Dialect, kind: &str) -> bool {
1340    match dialect.family {
1341        Family::Python | Family::GdScript => matches!(kind, "assignment" | "augmented_assignment"),
1342        Family::JsLike => matches!(
1343            kind,
1344            "assignment_expression" | "augmented_assignment_expression"
1345        ),
1346        Family::Rust => matches!(kind, "assignment_expression" | "compound_assignment_expr"),
1347    }
1348}
1349
1350fn names_bound_in_function_outside(
1351    dialect: Dialect,
1352    function: Node,
1353    start_byte: usize,
1354    end_byte: usize,
1355    source: &[u8],
1356) -> BTreeSet<String> {
1357    let mut names = BTreeSet::new();
1358    walk(function, &mut |node| {
1359        if node.start_byte() >= start_byte && node.end_byte() <= end_byte {
1360            return false;
1361        }
1362        if let Some(name) = binding_name(dialect, node, source) {
1363            names.insert(name);
1364        }
1365        true
1366    });
1367    // The function's own parameters bind for the whole body.
1368    if let Some(parameters) = function
1369        .child_by_field_name("parameters")
1370        .or_else(|| function.child_by_field_name("parameter"))
1371    {
1372        walk(parameters, &mut |node| {
1373            if is_type_position(dialect, node) {
1374                return false;
1375            }
1376            if is_name_node(dialect, node)
1377                && let Ok(text) = node.utf8_text(source)
1378            {
1379                names.insert(text.to_string());
1380            }
1381            true
1382        });
1383    }
1384    names
1385}
1386
1387/// Names read in the range before the range assigns them.
1388///
1389/// The "before" matters: a name the range assigns first is a local of the new
1390/// function, and passing it in would shadow that assignment with a stale value.
1391fn names_read_before_assignment(
1392    dialect: Dialect,
1393    statements: &[Node],
1394    source: &[u8],
1395) -> BTreeSet<String> {
1396    let mut assigned: BTreeSet<String> = BTreeSet::new();
1397    let mut read_first: BTreeSet<String> = BTreeSet::new();
1398    let mut events: Vec<(usize, bool, String)> = Vec::new();
1399    for statement in statements {
1400        walk(*statement, &mut |node| {
1401            if is_type_position(dialect, node) {
1402                return false;
1403            }
1404            if let Some(name) = binding_name(dialect, node, source) {
1405                let parent = node.parent();
1406                // An augmented assignment reads its target before writing it.
1407                if parent.is_some_and(|parent| is_augmented_assignment(dialect, parent.kind())) {
1408                    events.push((node.start_byte(), false, name.clone()));
1409                }
1410                // An assignment's write happens after its right-hand side is
1411                // evaluated, so `base = base * 2` reads the *outer* `base`.
1412                // Recording the write at the identifier's own offset would
1413                // classify that read as reading a local the range had already
1414                // assigned, and drop a parameter the new function needs.
1415                let write_at = parent
1416                    .filter(|parent| is_written_after_evaluation(dialect, parent.kind()))
1417                    .map(|parent| parent.end_byte())
1418                    .unwrap_or_else(|| node.start_byte());
1419                events.push((write_at, true, name));
1420                return true;
1421            }
1422            if is_read_identifier(dialect, node)
1423                && let Ok(text) = node.utf8_text(source)
1424            {
1425                events.push((node.start_byte(), false, text.to_string()));
1426            }
1427            true
1428        });
1429    }
1430    events.sort_by_key(|(offset, is_write, _)| (*offset, *is_write));
1431    for (_, is_write, name) in events {
1432        if is_write {
1433            assigned.insert(name);
1434        } else if !assigned.contains(&name) {
1435            read_first.insert(name);
1436        }
1437    }
1438    read_first
1439}
1440
1441fn names_read_after(
1442    dialect: Dialect,
1443    function: Node,
1444    end_byte: usize,
1445    source: &[u8],
1446) -> BTreeSet<String> {
1447    let mut names = BTreeSet::new();
1448    walk(function, &mut |node| {
1449        // Anything that ends at or before the range cannot contain a read that
1450        // starts after it.
1451        if node.end_byte() <= end_byte {
1452            return false;
1453        }
1454        if is_type_position(dialect, node) {
1455            return false;
1456        }
1457        if is_read_identifier(dialect, node)
1458            && node.start_byte() >= end_byte
1459            && let Ok(text) = node.utf8_text(source)
1460        {
1461            names.insert(text.to_string());
1462        }
1463        true
1464    });
1465    names
1466}
1467
1468/// The names bound directly by one scope's own statements.
1469///
1470/// Used twice, for two different scopes: the file root, whose names stay free
1471/// references rather than becoming parameters, and the block the new function
1472/// is inserted into, whose names it must not collide with.
1473fn scope_bindings(dialect: Dialect, scope: Node, source: &[u8]) -> BTreeSet<String> {
1474    let mut names = BTreeSet::new();
1475    let mut cursor = scope.walk();
1476    for statement in scope.named_children(&mut cursor) {
1477        if dialect.is_nested_scope_kind(statement.kind()) {
1478            if let Some(name) = statement
1479                .child_by_field_name("name")
1480                .and_then(|name| name.utf8_text(source).ok())
1481            {
1482                names.insert(name.to_string());
1483            }
1484            continue;
1485        }
1486        walk(statement, &mut |node| {
1487            if dialect.is_nested_scope_kind(node.kind()) {
1488                return false;
1489            }
1490            if let Some(name) = binding_name(dialect, node, source) {
1491                names.insert(name);
1492            }
1493            true
1494        });
1495    }
1496    names
1497}
1498
1499/// Whether this binder's write lands after the rest of the construct is read.
1500///
1501/// A loop head is deliberately absent: `for item in items` binds `item` before
1502/// the body runs, so a body read of `item` is not a read of an outer binding.
1503fn is_written_after_evaluation(dialect: Dialect, kind: &str) -> bool {
1504    if is_assignment_kind(dialect, kind) {
1505        return true;
1506    }
1507    match dialect.family {
1508        Family::Python => false,
1509        Family::GdScript => matches!(kind, "variable_statement" | "const_statement"),
1510        Family::JsLike => kind == "variable_declarator",
1511        Family::Rust => kind == "let_declaration",
1512    }
1513}
1514
1515fn is_augmented_assignment(dialect: Dialect, kind: &str) -> bool {
1516    match dialect.family {
1517        Family::Python | Family::GdScript => kind == "augmented_assignment",
1518        Family::JsLike => kind == "augmented_assignment_expression",
1519        Family::Rust => kind == "compound_assignment_expr",
1520    }
1521}
1522
1523/// Whether this node names something at all in this dialect, ignoring whether
1524/// the position reads or binds it.
1525fn is_name_node(dialect: Dialect, node: Node) -> bool {
1526    match dialect.family {
1527        Family::Python | Family::Rust => node.kind() == "identifier",
1528        Family::GdScript => matches!(node.kind(), "identifier" | "name"),
1529        Family::JsLike => matches!(
1530            node.kind(),
1531            "identifier" | "shorthand_property_identifier" | "shorthand_property_identifier_pattern"
1532        ),
1533    }
1534}
1535
1536/// A type annotation names types, not values, so nothing inside one is a read
1537/// or a binding of a runtime name.
1538fn is_type_position(dialect: Dialect, node: Node) -> bool {
1539    match dialect.family {
1540        Family::Python => matches!(node.kind(), "type"),
1541        Family::GdScript => matches!(node.kind(), "type" | "inferred_type"),
1542        Family::JsLike => matches!(node.kind(), "type_annotation" | "type_arguments"),
1543        // Rust spells a type as the `type` field of whatever binds it, with no
1544        // wrapper node of its own, so the field is the thing to recognize.
1545        Family::Rust => node
1546            .parent()
1547            .and_then(|parent| parent.child_by_field_name("type"))
1548            .is_some_and(|annotation| annotation.id() == node.id()),
1549    }
1550}
1551
1552/// The name this node binds, if it is a binding position.
1553fn binding_name(dialect: Dialect, node: Node, source: &[u8]) -> Option<String> {
1554    if !is_name_node(dialect, node) {
1555        return None;
1556    }
1557    let is_binding = match dialect.family {
1558        Family::Python => python_binds(dialect, node),
1559        Family::GdScript => gdscript_binds(node),
1560        Family::JsLike => js_binds(dialect, node),
1561        Family::Rust => rust_binds(dialect, node),
1562    };
1563    if !is_binding {
1564        return None;
1565    }
1566    node.utf8_text(source).ok().map(str::to_string)
1567}
1568
1569fn python_binds(dialect: Dialect, node: Node) -> bool {
1570    let Some(parent) = node.parent() else {
1571        return false;
1572    };
1573    match parent.kind() {
1574        // A direct child of the assignment is the target only when it *is* the
1575        // target: `obj.attr = 1` binds neither `obj` nor `attr`, and treating
1576        // them as bindings would make a receiver look like a local.
1577        "assignment" | "augmented_assignment" | "for_statement" => parent
1578            .child_by_field_name("left")
1579            .is_some_and(|left| left.id() == node.id()),
1580        "as_pattern_target" | "aliased_import" => true,
1581        "function_definition" | "class_definition" => parent
1582            .child_by_field_name("name")
1583            .is_some_and(|name| name.id() == node.id()),
1584        kind if dialect.pattern_kinds().contains(&kind) => pattern_root_binds(dialect, parent),
1585        _ => false,
1586    }
1587}
1588
1589fn gdscript_binds(node: Node) -> bool {
1590    let Some(parent) = node.parent() else {
1591        return false;
1592    };
1593    match parent.kind() {
1594        "variable_statement" | "const_statement" | "function_definition" | "class_definition"
1595        | "class_name_statement" | "signal_statement" | "enum_definition" => parent
1596            .child_by_field_name("name")
1597            .is_some_and(|name| name.id() == node.id()),
1598        "assignment" | "augmented_assignment" | "for_statement" => parent
1599            .child_by_field_name("left")
1600            .is_some_and(|left| left.id() == node.id()),
1601        "parameters" | "typed_parameter" | "typed_default_parameter" | "default_parameter" => true,
1602        _ => false,
1603    }
1604}
1605
1606fn js_binds(dialect: Dialect, node: Node) -> bool {
1607    if node.kind() == "shorthand_property_identifier_pattern" {
1608        return true;
1609    }
1610    let Some(parent) = node.parent() else {
1611        return false;
1612    };
1613    match parent.kind() {
1614        "variable_declarator" => parent
1615            .child_by_field_name("name")
1616            .is_some_and(|name| name.id() == node.id()),
1617        "assignment_expression" | "augmented_assignment_expression" | "for_in_statement" => parent
1618            .child_by_field_name("left")
1619            .is_some_and(|left| left.id() == node.id()),
1620        "function_declaration" | "generator_function_declaration" | "class_declaration"
1621        | "function_expression" | "import_specifier" | "namespace_import" | "catch_clause" => parent
1622            .child_by_field_name("name")
1623            .or_else(|| parent.child_by_field_name("parameter"))
1624            .is_some_and(|name| name.id() == node.id()),
1625        "formal_parameters" | "required_parameter" | "optional_parameter" => true,
1626        "arrow_function" => parent
1627            .child_by_field_name("parameter")
1628            .is_some_and(|name| name.id() == node.id()),
1629        kind if dialect.pattern_kinds().contains(&kind) => js_pattern_root_binds(dialect, parent),
1630        _ => false,
1631    }
1632}
1633
1634/// Whether a chain of pattern nodes bottoms out in a binding target.
1635fn pattern_root_binds(dialect: Dialect, node: Node) -> bool {
1636    let Some(parent) = node.parent() else {
1637        return false;
1638    };
1639    if dialect.pattern_kinds().contains(&parent.kind()) {
1640        return pattern_root_binds(dialect, parent);
1641    }
1642    match parent.kind() {
1643        "assignment" | "for_statement" => parent
1644            .child_by_field_name("left")
1645            .is_some_and(|left| left.id() == node.id()),
1646        _ => false,
1647    }
1648}
1649
1650fn js_pattern_root_binds(dialect: Dialect, node: Node) -> bool {
1651    let Some(parent) = node.parent() else {
1652        return false;
1653    };
1654    if dialect.pattern_kinds().contains(&parent.kind()) {
1655        return js_pattern_root_binds(dialect, parent);
1656    }
1657    match parent.kind() {
1658        "variable_declarator" => parent
1659            .child_by_field_name("name")
1660            .is_some_and(|name| name.id() == node.id()),
1661        "assignment_expression" | "for_in_statement" => parent
1662            .child_by_field_name("left")
1663            .is_some_and(|left| left.id() == node.id()),
1664        "formal_parameters" | "required_parameter" | "optional_parameter" | "arrow_function" => {
1665            true
1666        }
1667        _ => false,
1668    }
1669}
1670
1671/// Rust binding positions.
1672///
1673/// Every one of them is a `pattern` or a `name` field, which is what makes the
1674/// receiver case cheap to get right: `self` is its own node kind, never an
1675/// `identifier`, so it can never be mistaken for a name a signature could
1676/// carry.
1677fn rust_binds(dialect: Dialect, node: Node) -> bool {
1678    let Some(parent) = node.parent() else {
1679        return false;
1680    };
1681    match parent.kind() {
1682        "let_declaration" | "for_expression" | "parameter" | "closure_parameters" => parent
1683            .child_by_field_name("pattern")
1684            .is_some_and(|pattern| pattern.id() == node.id())
1685            || parent.kind() == "closure_parameters",
1686        "assignment_expression" | "compound_assignment_expr" => parent
1687            .child_by_field_name("left")
1688            .is_some_and(|left| left.id() == node.id()),
1689        "function_item" | "const_item" | "static_item" | "mod_item" => parent
1690            .child_by_field_name("name")
1691            .is_some_and(|name| name.id() == node.id()),
1692        kind if dialect.pattern_kinds().contains(&kind) => rust_pattern_root_binds(dialect, parent),
1693        _ => false,
1694    }
1695}
1696
1697fn rust_pattern_root_binds(dialect: Dialect, node: Node) -> bool {
1698    let Some(parent) = node.parent() else {
1699        return false;
1700    };
1701    if dialect.pattern_kinds().contains(&parent.kind()) {
1702        return rust_pattern_root_binds(dialect, parent);
1703    }
1704    match parent.kind() {
1705        "let_declaration" | "for_expression" | "parameter" => parent
1706            .child_by_field_name("pattern")
1707            .is_some_and(|pattern| pattern.id() == node.id()),
1708        "closure_parameters" => true,
1709        _ => false,
1710    }
1711}
1712
1713fn rust_reads(dialect: Dialect, node: Node, parent: Node) -> bool {
1714    match parent.kind() {
1715        // `value.field` — the field is a `field_identifier`, so only the
1716        // receiver reaches here.
1717        "field_expression" => parent
1718            .child_by_field_name("field")
1719            .is_none_or(|field| field.id() != node.id()),
1720        "let_declaration" | "for_expression" | "parameter" => parent
1721            .child_by_field_name("pattern")
1722            .is_none_or(|pattern| !covers(pattern, node)),
1723        "assignment_expression" | "compound_assignment_expr" => parent
1724            .child_by_field_name("left")
1725            .is_none_or(|left| !covers(left, node)),
1726        "function_item" | "const_item" | "static_item" | "mod_item" => parent
1727            .child_by_field_name("name")
1728            .is_none_or(|name| name.id() != node.id()),
1729        "closure_parameters" => false,
1730        kind if dialect.pattern_kinds().contains(&kind) => !rust_pattern_root_binds(dialect, parent),
1731        _ => true,
1732    }
1733}
1734
1735/// Whether this identifier reads a binding, as opposed to naming a member, a
1736/// keyword argument, or a binding position.
1737fn is_read_identifier(dialect: Dialect, node: Node) -> bool {
1738    if !is_name_node(dialect, node) {
1739        return false;
1740    }
1741    if node.kind() == "shorthand_property_identifier_pattern" {
1742        return false;
1743    }
1744    let Some(parent) = node.parent() else {
1745        return false;
1746    };
1747    match dialect.family {
1748        Family::Python => python_reads(dialect, node, parent),
1749        Family::GdScript => gdscript_reads(node, parent),
1750        Family::JsLike => js_reads(dialect, node, parent),
1751        Family::Rust => rust_reads(dialect, node, parent),
1752    }
1753}
1754
1755fn python_reads(dialect: Dialect, node: Node, parent: Node) -> bool {
1756    match parent.kind() {
1757        // `obj.name` — the member is not a binding in this scope.
1758        "attribute" => parent
1759            .child_by_field_name("attribute")
1760            .is_none_or(|attribute| attribute.id() != node.id()),
1761        // `f(name=value)` — the keyword is the callee's parameter name.
1762        "keyword_argument" => parent
1763            .child_by_field_name("name")
1764            .is_none_or(|name| name.id() != node.id()),
1765        "assignment" | "for_statement" => parent
1766            .child_by_field_name("left")
1767            .is_none_or(|left| !covers(left, node)),
1768        // An augmented assignment reads its target; `names_read_before_assignment`
1769        // records that read explicitly, so it is not double-counted here.
1770        "augmented_assignment" => parent
1771            .child_by_field_name("left")
1772            .is_none_or(|left| !covers(left, node)),
1773        "function_definition" | "class_definition" => parent
1774            .child_by_field_name("name")
1775            .is_none_or(|name| name.id() != node.id()),
1776        "parameters" | "default_parameter" | "typed_parameter" | "as_pattern_target"
1777        | "aliased_import" => false,
1778        kind if dialect.pattern_kinds().contains(&kind) => !pattern_root_binds(dialect, parent),
1779        _ => true,
1780    }
1781}
1782
1783fn gdscript_reads(node: Node, parent: Node) -> bool {
1784    // GDScript spells binding positions with a `name` node and reads with an
1785    // `identifier`, so most of the work is already done by the grammar.
1786    if node.kind() == "name" {
1787        return false;
1788    }
1789    match parent.kind() {
1790        // `(attribute (identifier) (identifier))` carries no field names: the
1791        // first child is the receiver and reads, the rest are members.
1792        "attribute" => parent
1793            .named_child(0)
1794            .is_some_and(|object| object.id() == node.id()),
1795        "assignment" | "augmented_assignment" | "for_statement" => parent
1796            .child_by_field_name("left")
1797            .is_none_or(|left| left.id() != node.id()),
1798        "parameters" | "typed_parameter" | "typed_default_parameter" | "default_parameter"
1799        | "type" | "inferred_type" => false,
1800        _ => true,
1801    }
1802}
1803
1804fn js_reads(dialect: Dialect, node: Node, parent: Node) -> bool {
1805    match parent.kind() {
1806        // `obj.name` — the property is spelled `property_identifier`, so only a
1807        // computed member's index reaches here, and that does read.
1808        "member_expression" => parent
1809            .child_by_field_name("property")
1810            .is_none_or(|property| property.id() != node.id()),
1811        "variable_declarator" => parent
1812            .child_by_field_name("name")
1813            .is_none_or(|name| name.id() != node.id()),
1814        "assignment_expression" | "augmented_assignment_expression" | "for_in_statement" => parent
1815            .child_by_field_name("left")
1816            .is_none_or(|left| !covers(left, node)),
1817        "function_declaration" | "generator_function_declaration" | "class_declaration"
1818        | "function_expression" => parent
1819            .child_by_field_name("name")
1820            .is_none_or(|name| name.id() != node.id()),
1821        "formal_parameters" | "required_parameter" | "optional_parameter" | "import_specifier"
1822        | "namespace_import" | "catch_clause" | "labeled_statement" => false,
1823        "arrow_function" => parent
1824            .child_by_field_name("parameter")
1825            .is_none_or(|name| name.id() != node.id()),
1826        kind if dialect.pattern_kinds().contains(&kind) => !js_pattern_root_binds(dialect, parent),
1827        _ => true,
1828    }
1829}
1830
1831fn covers(outer: Node, inner: Node) -> bool {
1832    outer.id() == inner.id()
1833        || (outer.start_byte() <= inner.start_byte() && outer.end_byte() >= inner.end_byte())
1834}
1835
1836/// Pre-order walk; the visitor returns `false` to skip a subtree.
1837fn walk(node: Node, visit: &mut impl FnMut(Node) -> bool) {
1838    if !visit(node) {
1839        return;
1840    }
1841    let mut cursor = node.walk();
1842    for child in node.named_children(&mut cursor) {
1843        walk(child, visit);
1844    }
1845}
1846
1847/// The whitespace prefix of the line `byte` sits on.
1848fn line_indent(source: &[u8], byte: usize) -> String {
1849    let line_start = source[..byte]
1850        .iter()
1851        .rposition(|byte| *byte == b'\n')
1852        .map(|position| position + 1)
1853        .unwrap_or(0);
1854    String::from_utf8_lossy(&source[line_start..byte])
1855        .chars()
1856        .take_while(|character| character.is_whitespace())
1857        .collect()
1858}
1859
1860#[cfg(all(test, feature = "lang-python"))]
1861mod python_tests {
1862    use super::*;
1863
1864    const SOURCE: &str = "TOTAL = 10\n\n\ndef outer(base, scale):\n    prefix = base * 2\n    acc = 0\n    for item in range(scale):\n        acc += item * prefix\n    label = f\"{acc}\"\n    return label, acc, TOTAL\n";
1865
1866    fn plan(start: usize, end: usize, name: &str) -> Result<ExtractionPlan, ExtractionRefusal> {
1867        plan_extraction(Lang::Python, SOURCE.as_bytes(), start, end, name)
1868    }
1869
1870    #[test]
1871    fn derives_parameters_from_outer_bindings_and_returns_from_later_reads() {
1872        // Lines 5-7: `acc = 0` and the `for` loop. `prefix` and `scale` come
1873        // from outside; `acc` is assigned here and read on line 8.
1874        let plan = plan(5, 7, "accumulate").expect("planned");
1875
1876        assert_eq!(plan.enclosing_function, "outer");
1877        assert_eq!(
1878            plan.parameters,
1879            vec!["prefix".to_string(), "scale".to_string()]
1880        );
1881        assert_eq!(plan.parameter_spellings, plan.parameters);
1882        assert_eq!(plan.returns, vec!["acc".to_string()]);
1883        assert_eq!(plan.indent, "    ");
1884        assert_eq!(plan.indent_unit, "    ");
1885        // Python has no declarations, so a call site never declares.
1886        assert!(!plan.returns_need_declaration);
1887    }
1888
1889    #[test]
1890    fn a_name_the_range_assigns_first_is_a_local_not_a_parameter() {
1891        // `acc` is bound *outside* the range on line 1, so the outer-binding
1892        // test alone would make it a parameter. It is not one: line 2 assigns
1893        // it before line 3 reads it, so passing the outer value in would feed
1894        // the extracted body a value it immediately overwrites, and the caller
1895        // would keep computing an argument nothing reads.
1896        let source =
1897            "def outer(base):\n    acc = 0\n    acc = base * 2\n    acc += 1\n    return acc\n";
1898        let plan =
1899            plan_extraction(Lang::Python, source.as_bytes(), 2, 3, "recompute").expect("planned");
1900
1901        assert_eq!(plan.parameters, vec!["base".to_string()]);
1902        assert_eq!(plan.returns, vec!["acc".to_string()]);
1903    }
1904
1905    #[test]
1906    fn a_module_scope_name_is_neither_parameter_nor_return() {
1907        // Extract only the `label` line, which reads `acc` and `TOTAL`. `TOTAL`
1908        // is bound at module scope, so it is not in the enclosing function's
1909        // bindings and stays a free reference. Threading it through a parameter
1910        // would compile and quietly change what the new function closes over —
1911        // which is also why `plan_extraction` checks module scope for the
1912        // *collision* case rather than trusting "not bound in the function".
1913        let plan = plan(8, 8, "describe").expect("planned");
1914        assert_eq!(plan.parameters, vec!["acc".to_string()]);
1915        assert!(!plan.parameters.contains(&"TOTAL".to_string()));
1916        assert_eq!(plan.returns, vec!["label".to_string()]);
1917    }
1918
1919    #[test]
1920    fn an_attribute_assignment_binds_neither_the_receiver_nor_the_member() {
1921        // `cfg.limit = base` writes through `cfg`; it does not rebind it. If
1922        // the receiver counted as assigned-in-range it would stop being a
1923        // parameter, and the extracted function would write to a name it never
1924        // received — which raises at the call site rather than at review.
1925        let source = "def outer(cfg, base):\n    cfg.limit = base\n    return cfg\n";
1926        let plan =
1927            plan_extraction(Lang::Python, source.as_bytes(), 1, 1, "configure").expect("planned");
1928        assert_eq!(
1929            plan.parameters,
1930            vec!["base".to_string(), "cfg".to_string()]
1931        );
1932        assert!(plan.returns.is_empty(), "{:?}", plan.returns);
1933    }
1934
1935    #[test]
1936    fn refuses_a_range_whose_control_flow_escapes() {
1937        assert_eq!(
1938            plan(9, 9, "finish"),
1939            Err(ExtractionRefusal::ControlFlowEscapes("return"))
1940        );
1941    }
1942
1943    #[test]
1944    fn a_break_bound_to_a_loop_inside_the_range_does_not_escape() {
1945        // The loop moves with the range, so its `break` still breaks the same
1946        // loop. Refusing here declined the most ordinary extraction there is.
1947        let source = "def outer(items, limit):\n    total = 0\n    for item in items:\n        if item > limit:\n            break\n        total += item\n    return total\n";
1948        let plan =
1949            plan_extraction(Lang::Python, source.as_bytes(), 1, 5, "sum_until").expect("planned");
1950        assert_eq!(plan.parameters, vec!["items".to_string(), "limit".to_string()]);
1951        assert_eq!(plan.returns, vec!["total".to_string()]);
1952    }
1953
1954    #[test]
1955    fn a_break_bound_to_a_loop_outside_the_range_still_escapes() {
1956        // Same keyword, opposite answer: the loop stays behind, so hoisting the
1957        // `break` changes which construct it leaves.
1958        let source = "def outer(items, limit):\n    total = 0\n    for item in items:\n        if item > limit:\n            break\n        total += item\n    return total\n";
1959        assert_eq!(
1960            plan_extraction(Lang::Python, source.as_bytes(), 3, 5, "accumulate"),
1961            Err(ExtractionRefusal::ControlFlowEscapes("break"))
1962        );
1963    }
1964
1965    #[test]
1966    fn a_continue_bound_to_a_loop_inside_the_range_does_not_escape() {
1967        let source = "def outer(items):\n    total = 0\n    for item in items:\n        if item < 0:\n            continue\n        total += item\n    return total\n";
1968        let plan =
1969            plan_extraction(Lang::Python, source.as_bytes(), 1, 5, "sum_positive").expect("planned");
1970        assert_eq!(plan.returns, vec!["total".to_string()]);
1971    }
1972
1973    #[test]
1974    fn refuses_a_range_outside_any_function() {
1975        assert_eq!(
1976            plan(0, 0, "setup"),
1977            Err(ExtractionRefusal::NotInsideFunction)
1978        );
1979    }
1980
1981    #[test]
1982    fn refuses_an_empty_range() {
1983        assert_eq!(plan(1, 2, "nothing"), Err(ExtractionRefusal::EmptyRange));
1984    }
1985
1986    #[test]
1987    fn refuses_a_name_that_already_binds_at_module_scope() {
1988        assert_eq!(
1989            plan(5, 7, "TOTAL"),
1990            Err(ExtractionRefusal::NameCollision("TOTAL".to_string()))
1991        );
1992    }
1993
1994    #[test]
1995    fn refuses_a_name_that_already_binds_in_the_enclosing_function() {
1996        assert_eq!(
1997            plan(5, 7, "prefix"),
1998            Err(ExtractionRefusal::NameCollision("prefix".to_string()))
1999        );
2000    }
2001
2002    #[test]
2003    fn refuses_when_the_range_assigns_a_global_declared_name() {
2004        let source =
2005            "COUNT = 0\n\n\ndef outer():\n    global COUNT\n    COUNT = 1\n    return COUNT\n";
2006        assert_eq!(
2007            plan_extraction(Lang::Python, source.as_bytes(), 5, 5, "bump"),
2008            Err(ExtractionRefusal::RebindsOuterScope("COUNT".to_string()))
2009        );
2010    }
2011
2012    #[test]
2013    fn a_method_extraction_lands_past_the_class_with_self_as_a_parameter() {
2014        // A `def` placed *beside* a method is another method, and the bare call
2015        // left in its place does not resolve to it. Climbing past the class
2016        // puts it where the call can see it, and `self` — a name like any
2017        // other in Python — threads through the signature.
2018        let source = "class Panel:\n    def outer(self, base):\n        acc = self.scale * base\n        return acc\n";
2019        let plan =
2020            plan_extraction(Lang::Python, source.as_bytes(), 2, 2, "double").expect("planned");
2021        assert_eq!(plan.enclosing_function, "outer");
2022        // Receiver first: alphabetical order would read as a mistake.
2023        assert_eq!(
2024            plan.parameters,
2025            vec!["self".to_string(), "base".to_string()]
2026        );
2027        // Module scope, not class scope.
2028        assert_eq!(plan.enclosing_indent, "");
2029        assert_eq!(plan.insert_byte, source.len() - 1);
2030        let (function, call) = render_extraction(&plan, source, "double");
2031        assert!(function.contains("\ndef double(self, base):"), "{function}");
2032        assert!(
2033            function.contains("\n    acc = self.scale * base"),
2034            "{function}"
2035        );
2036        assert_eq!(call, "        acc = double(self, base)");
2037    }
2038
2039    #[test]
2040    fn a_nested_function_extraction_stays_inside_its_enclosing_function() {
2041        // Not every climb is out to module scope: a nested `def` closes over
2042        // the outer function's locals, and hoisting past it would leave the
2043        // extracted body reading names that are no longer in scope.
2044        let source = "def outer(a):\n    scale = 2\n\n    def inner(b):\n        acc = scale * b\n        return acc\n    return inner\n";
2045        let plan =
2046            plan_extraction(Lang::Python, source.as_bytes(), 4, 4, "double").expect("planned");
2047        assert_eq!(plan.enclosing_function, "inner");
2048        assert_eq!(plan.enclosing_indent, "    ");
2049        // `scale` belongs to `outer`, not to `inner`, so it stays a free
2050        // reference the sibling `def` can still see.
2051        assert_eq!(plan.parameters, vec!["b".to_string()]);
2052    }
2053
2054    #[test]
2055    fn renders_a_def_and_a_destructuring_call_that_agree() {
2056        let plan = plan(5, 7, "accumulate").expect("planned");
2057        let (function, call) = render_extraction(&plan, SOURCE, "accumulate");
2058
2059        assert!(
2060            function.contains("def accumulate(prefix, scale):"),
2061            "{function}"
2062        );
2063        assert!(function.contains("    acc = 0"), "{function}");
2064        assert!(
2065            function.contains("        acc += item * prefix"),
2066            "{function}"
2067        );
2068        assert!(function.contains("    return acc"), "{function}");
2069        assert_eq!(call, "    acc = accumulate(prefix, scale)");
2070    }
2071
2072    #[test]
2073    fn renders_a_bare_call_when_nothing_is_read_afterwards() {
2074        let source = "def outer(scale):\n    total = 0\n    print(scale)\n    return total\n";
2075        let plan =
2076            plan_extraction(Lang::Python, source.as_bytes(), 2, 2, "report").expect("planned");
2077        assert!(plan.returns.is_empty(), "{:?}", plan.returns);
2078        let (function, call) = render_extraction(&plan, source, "report");
2079        assert!(function.contains("def report(scale):"), "{function}");
2080        assert!(!function.contains("return"), "{function}");
2081        assert_eq!(call, "    report(scale)");
2082    }
2083
2084    #[test]
2085    fn indentation_is_measured_from_the_file_rather_than_assumed() {
2086        // Two-space Python is unusual and legal. Emitting four spaces here
2087        // would still parse — and would leave a file that no longer agrees
2088        // with itself.
2089        let source = "def outer(base):\n  acc = base * 2\n  return acc\n";
2090        let plan =
2091            plan_extraction(Lang::Python, source.as_bytes(), 1, 1, "double").expect("planned");
2092        assert_eq!(plan.indent_unit, "  ");
2093        let (function, _) = render_extraction(&plan, source, "double");
2094        assert!(function.contains("\n  acc = base * 2"), "{function}");
2095    }
2096}
2097
2098#[cfg(all(test, feature = "lang-gdscript"))]
2099mod gdscript_tests {
2100    use super::*;
2101
2102    const SOURCE: &str = "const TOTAL = 10\n\nfunc outer(base, scale):\n\tvar prefix = base * 2\n\tvar acc = 0\n\tfor item in range(scale):\n\t\tacc += item * prefix\n\treturn acc\n";
2103
2104    #[test]
2105    fn derives_the_same_signature_the_python_core_does() {
2106        let plan =
2107            plan_extraction(Lang::GdScript, SOURCE.as_bytes(), 4, 6, "accumulate").expect("planned");
2108        assert_eq!(plan.enclosing_function, "outer");
2109        assert_eq!(
2110            plan.parameters,
2111            vec!["prefix".to_string(), "scale".to_string()]
2112        );
2113        assert_eq!(plan.returns, vec!["acc".to_string()]);
2114        assert_eq!(plan.indent_unit, "\t");
2115        // `var acc` left with the range, so the call site has to declare it.
2116        assert!(plan.returns_need_declaration);
2117    }
2118
2119    #[test]
2120    fn renders_a_func_and_a_var_call_that_agree() {
2121        let plan =
2122            plan_extraction(Lang::GdScript, SOURCE.as_bytes(), 4, 6, "accumulate").expect("planned");
2123        let (function, call) = render_extraction(&plan, SOURCE, "accumulate");
2124        assert!(
2125            function.contains("func accumulate(prefix, scale):"),
2126            "{function}"
2127        );
2128        assert!(function.contains("\n\tvar acc = 0"), "{function}");
2129        assert!(
2130            function.contains("\n\t\tacc += item * prefix"),
2131            "{function}"
2132        );
2133        assert!(function.contains("\n\treturn acc"), "{function}");
2134        assert_eq!(call, "\tvar acc = accumulate(prefix, scale)");
2135    }
2136
2137    #[test]
2138    fn assigns_rather_than_declares_when_the_name_outlives_the_range() {
2139        // `acc` is declared before the range, so the range only assigns it. A
2140        // call site that said `var acc = ...` would shadow the outer binding
2141        // and the later read would see the stale value.
2142        let source = "func outer(base):\n\tvar acc = 0\n\tacc = base * 2\n\treturn acc\n";
2143        let plan =
2144            plan_extraction(Lang::GdScript, source.as_bytes(), 2, 2, "double").expect("planned");
2145        assert!(!plan.returns_need_declaration);
2146        // The declaration stayed behind, so the *new* function has to make one:
2147        // its body assigns `acc`, and GDScript will not accept that bare.
2148        assert_eq!(plan.local_declarations, vec!["acc".to_string()]);
2149        let (function, call) = render_extraction(&plan, source, "double");
2150        assert!(function.contains("\n\tvar acc\n\tacc = base * 2"), "{function}");
2151        assert_eq!(call, "\tacc = double(base)");
2152    }
2153
2154    #[test]
2155    fn refuses_a_range_that_assigns_a_file_scope_var() {
2156        // A bare `acc = ...` inside a `func` writes the script's own `var acc`.
2157        // Declaring it in the new function would shadow that member and the
2158        // write would stop being visible — a change no reader would see.
2159        let source = "var acc = 0\n\nfunc outer(base):\n\tacc = base * 2\n\treturn acc\n";
2160        assert_eq!(
2161            plan_extraction(Lang::GdScript, source.as_bytes(), 3, 3, "double"),
2162            Err(ExtractionRefusal::AssignsUndeclaredName("acc".to_string()))
2163        );
2164    }
2165
2166    #[test]
2167    fn a_write_through_a_member_leaves_the_receiver_a_parameter() {
2168        let source = "func outer(cfg, base):\n\tcfg.limit = base\n\treturn cfg\n";
2169        let plan =
2170            plan_extraction(Lang::GdScript, source.as_bytes(), 1, 1, "configure").expect("planned");
2171        assert_eq!(
2172            plan.parameters,
2173            vec!["base".to_string(), "cfg".to_string()]
2174        );
2175        assert!(plan.returns.is_empty(), "{:?}", plan.returns);
2176    }
2177
2178    #[test]
2179    fn refuses_more_than_one_returned_name() {
2180        // GDScript has no destructuring assignment, so two values cannot reach
2181        // one call site. Emitting an array and two index reads would be three
2182        // statements where the caller wrote one.
2183        let source =
2184            "func outer(base):\n\tvar a = base\n\tvar b = base + 1\n\treturn a + b\n";
2185        assert_eq!(
2186            plan_extraction(Lang::GdScript, source.as_bytes(), 1, 2, "split"),
2187            Err(ExtractionRefusal::MultipleReturnsUnsupported("gdscript"))
2188        );
2189    }
2190
2191    #[test]
2192    fn a_method_extraction_stays_inside_the_class_as_a_sibling_func() {
2193        // The opposite of Python and JavaScript, and for the opposite reason:
2194        // GDScript resolves a bare call against the script's own members, so a
2195        // sibling `func` is exactly what the call left behind needs. Climbing
2196        // out of the class would break the call rather than fix it.
2197        let source = "class Panel:\n\tfunc outer(base):\n\t\tvar acc = base * 2\n\t\treturn acc\n";
2198        let plan =
2199            plan_extraction(Lang::GdScript, source.as_bytes(), 2, 2, "double").expect("planned");
2200        assert_eq!(plan.enclosing_function, "outer");
2201        assert_eq!(plan.enclosing_indent, "\t");
2202        let (function, call) = render_extraction(&plan, source, "double");
2203        assert!(function.contains("\n\tfunc double(base):"), "{function}");
2204        assert!(function.contains("\n\t\tvar acc = base * 2"), "{function}");
2205        assert_eq!(call, "\t\tvar acc = double(base)");
2206    }
2207
2208    #[test]
2209    fn refuses_a_name_that_already_binds_as_a_sibling_method() {
2210        // The new `func` lands inside the class, so the names it must not
2211        // collide with are the class's own members — which a file-root scan
2212        // never sees.
2213        let source = "class Panel:\n\tfunc double(x):\n\t\treturn x\n\n\tfunc outer(base):\n\t\tvar acc = base * 2\n\t\treturn acc\n";
2214        assert_eq!(
2215            plan_extraction(Lang::GdScript, source.as_bytes(), 5, 5, "double"),
2216            Err(ExtractionRefusal::NameCollision("double".to_string()))
2217        );
2218    }
2219
2220    #[test]
2221    fn refuses_a_range_whose_control_flow_escapes() {
2222        assert_eq!(
2223            plan_extraction(Lang::GdScript, SOURCE.as_bytes(), 7, 7, "finish"),
2224            Err(ExtractionRefusal::ControlFlowEscapes("return"))
2225        );
2226    }
2227}
2228
2229#[cfg(all(test, feature = "lang-javascript"))]
2230mod javascript_tests {
2231    use super::*;
2232
2233    const SOURCE: &str = "const TOTAL = 10;\n\nfunction outer(base, scale) {\n  const prefix = base * 2;\n  let acc = 0;\n  for (const item of range(scale)) {\n    acc += item * prefix;\n  }\n  return acc + TOTAL;\n}\n";
2234
2235    #[test]
2236    fn derives_the_same_signature_the_python_core_does() {
2237        let plan = plan_extraction(Lang::JavaScript, SOURCE.as_bytes(), 4, 7, "accumulate")
2238            .expect("planned");
2239        assert_eq!(plan.enclosing_function, "outer");
2240        assert_eq!(
2241            plan.parameters,
2242            vec!["prefix".to_string(), "scale".to_string()]
2243        );
2244        assert_eq!(plan.returns, vec!["acc".to_string()]);
2245        assert_eq!(plan.indent_unit, "  ");
2246        assert!(plan.returns_need_declaration);
2247    }
2248
2249    #[test]
2250    fn renders_a_function_and_a_let_call_that_agree() {
2251        let plan = plan_extraction(Lang::JavaScript, SOURCE.as_bytes(), 4, 7, "accumulate")
2252            .expect("planned");
2253        let (function, call) = render_extraction(&plan, SOURCE, "accumulate");
2254        assert!(
2255            function.contains("function accumulate(prefix, scale) {"),
2256            "{function}"
2257        );
2258        assert!(function.contains("\n  let acc = 0;"), "{function}");
2259        assert!(function.contains("\n    acc += item * prefix;"), "{function}");
2260        assert!(function.contains("\n  return acc;"), "{function}");
2261        assert!(function.ends_with("}\n"), "{function}");
2262        assert_eq!(call, "  let acc = accumulate(prefix, scale);");
2263    }
2264
2265    #[test]
2266    fn several_returned_names_become_one_array_destructuring() {
2267        let source = "function outer(base) {\n  let a = base;\n  let b = base + 1;\n  return a + b;\n}\n";
2268        let plan =
2269            plan_extraction(Lang::JavaScript, source.as_bytes(), 1, 2, "split").expect("planned");
2270        assert_eq!(plan.returns, vec!["a".to_string(), "b".to_string()]);
2271        let (function, call) = render_extraction(&plan, source, "split");
2272        assert!(function.contains("return [a, b];"), "{function}");
2273        assert_eq!(call, "  let [a, b] = split(base);");
2274    }
2275
2276    #[test]
2277    fn a_bare_call_still_ends_in_a_semicolon() {
2278        let source = "function outer(scale) {\n  report(scale);\n  return 1;\n}\n";
2279        let plan =
2280            plan_extraction(Lang::JavaScript, source.as_bytes(), 1, 1, "announce").expect("planned");
2281        assert!(plan.returns.is_empty(), "{:?}", plan.returns);
2282        let (_, call) = render_extraction(&plan, source, "announce");
2283        assert_eq!(call, "  announce(scale);");
2284    }
2285
2286    #[test]
2287    fn a_property_write_leaves_the_receiver_a_parameter() {
2288        let source = "function outer(cfg, base) {\n  cfg.limit = base;\n  return cfg;\n}\n";
2289        let plan =
2290            plan_extraction(Lang::JavaScript, source.as_bytes(), 1, 1, "configure").expect("planned");
2291        assert_eq!(
2292            plan.parameters,
2293            vec!["base".to_string(), "cfg".to_string()]
2294        );
2295        assert!(plan.returns.is_empty(), "{:?}", plan.returns);
2296    }
2297
2298    #[test]
2299    fn refuses_a_range_that_mixes_new_and_existing_names() {
2300        // `a` exists before the range and `b` is created inside it. One call
2301        // site cannot both assign and declare, and two would rebind `a` before
2302        // the second statement read it.
2303        let source = "function outer(base) {\n  let a = 0;\n  a = base;\n  let b = base + 1;\n  return a + b;\n}\n";
2304        assert_eq!(
2305            plan_extraction(Lang::JavaScript, source.as_bytes(), 2, 3, "split"),
2306            Err(ExtractionRefusal::MixedReturnDeclarations)
2307        );
2308    }
2309
2310    #[test]
2311    fn refuses_a_range_that_assigns_a_module_scope_binding() {
2312        let source = "let total = 0;\n\nfunction outer(base) {\n  total = base * 2;\n  return total;\n}\n";
2313        assert_eq!(
2314            plan_extraction(Lang::JavaScript, source.as_bytes(), 3, 3, "double"),
2315            Err(ExtractionRefusal::AssignsUndeclaredName("total".to_string()))
2316        );
2317    }
2318
2319    #[test]
2320    fn a_self_referential_assignment_keeps_its_target_a_parameter() {
2321        // `base = base * 2` reads the *outer* `base` before writing it. An
2322        // ordering that recorded the write at the target's own offset would
2323        // classify that read as reading a local the range had already assigned,
2324        // drop `base` from the signature, and emit a function that multiplies
2325        // `undefined`.
2326        let source =
2327            "function outer(base) {\n  base = base * 2;\n  return base;\n}\n";
2328        let plan =
2329            plan_extraction(Lang::JavaScript, source.as_bytes(), 1, 1, "double").expect("planned");
2330        assert_eq!(plan.parameters, vec!["base".to_string()]);
2331        assert_eq!(plan.returns, vec!["base".to_string()]);
2332        // Already declared by the signature, so no prologue.
2333        assert!(plan.local_declarations.is_empty());
2334        let (function, call) = render_extraction(&plan, source, "double");
2335        assert!(function.contains("function double(base) {"), "{function}");
2336        assert_eq!(call, "  base = double(base);");
2337    }
2338
2339    #[test]
2340    fn a_method_extraction_lands_beside_the_class_declaration() {
2341        let source =
2342            "class Panel {\n  outer(base) {\n    let acc = base * 2;\n    return acc;\n  }\n}\n";
2343        let plan =
2344            plan_extraction(Lang::JavaScript, source.as_bytes(), 2, 2, "double").expect("planned");
2345        assert_eq!(plan.enclosing_function, "outer");
2346        // Beside the class, at the class's own indentation — not inside the
2347        // class body, where `function double(...)` is not even legal.
2348        assert_eq!(plan.enclosing_indent, "");
2349        assert_eq!(plan.insert_byte, source.len() - 1);
2350        let (function, call) = render_extraction(&plan, source, "double");
2351        assert!(function.contains("\nfunction double(base) {"), "{function}");
2352        assert_eq!(call, "    let acc = double(base);");
2353    }
2354
2355    #[test]
2356    fn a_break_bound_to_a_switch_inside_the_range_does_not_escape() {
2357        // JavaScript's `break` binds to a `switch` as well as to a loop, so the
2358        // loop-depth test alone would refuse a hoisted switch that is entirely
2359        // self-contained.
2360        let source = "function outer(kind) {\n  let label = \"\";\n  switch (kind) {\n    case 1:\n      label = \"one\";\n      break;\n    default:\n      label = \"other\";\n  }\n  return label;\n}\n";
2361        let plan =
2362            plan_extraction(Lang::JavaScript, source.as_bytes(), 2, 8, "describe").expect("planned");
2363        assert_eq!(plan.parameters, vec!["kind".to_string()]);
2364        assert_eq!(plan.returns, vec!["label".to_string()]);
2365    }
2366
2367    #[test]
2368    fn a_labelled_break_targeting_a_label_outside_the_range_escapes() {
2369        // The inner loop moves with the range, so an unlabelled `break` would
2370        // be fine — but this one jumps to a label that stays behind, and no
2371        // signature carries a jump out of two loops.
2372        let source = "function outer(rows) {\n  let hits = 0;\n  outer: for (const row of rows) {\n    for (const cell of row) {\n      if (cell) {\n        break outer;\n      }\n      hits += 1;\n    }\n  }\n  return hits;\n}\n";
2373        assert_eq!(
2374            plan_extraction(Lang::JavaScript, source.as_bytes(), 3, 8, "scan"),
2375            Err(ExtractionRefusal::ControlFlowEscapes("break"))
2376        );
2377    }
2378
2379    #[test]
2380    fn a_labelled_break_whose_label_is_inside_the_range_does_not_escape() {
2381        let source = "function outer(rows) {\n  let hits = 0;\n  outer: for (const row of rows) {\n    for (const cell of row) {\n      if (cell) {\n        break outer;\n      }\n      hits += 1;\n    }\n  }\n  return hits;\n}\n";
2382        let plan =
2383            plan_extraction(Lang::JavaScript, source.as_bytes(), 1, 9, "scan").expect("planned");
2384        assert_eq!(plan.parameters, vec!["rows".to_string()]);
2385        assert_eq!(plan.returns, vec!["hits".to_string()]);
2386        assert!(plan.returns_need_declaration);
2387    }
2388
2389    #[test]
2390    fn refuses_a_method_extraction_that_uses_this() {
2391        // `this` is not a name a derived signature can carry, and a plain
2392        // function's `this` is not the method's. Threading it would take a
2393        // body rewrite the derivation does not do, so it refuses instead of
2394        // emitting a function that reads a different receiver.
2395        let source = "class Panel {\n  outer(base) {\n    let acc = this.scale * base;\n    return acc;\n  }\n}\n";
2396        assert_eq!(
2397            plan_extraction(Lang::JavaScript, source.as_bytes(), 2, 2, "double"),
2398            Err(ExtractionRefusal::ReferencesReceiver("this"))
2399        );
2400    }
2401
2402    #[test]
2403    fn refuses_to_hoist_out_of_an_arrow_function() {
2404        // `const view = () => {...}` has no statement position beside the
2405        // arrow: inserting there would land inside the declaration.
2406        let source = "const view = (base) => {\n  let acc = base * 2;\n  return acc;\n};\n";
2407        assert_eq!(
2408            plan_extraction(Lang::JavaScript, source.as_bytes(), 1, 1, "double"),
2409            Err(ExtractionRefusal::EnclosingFunctionNotHoistable)
2410        );
2411    }
2412}
2413
2414#[cfg(all(test, feature = "lang-typescript"))]
2415mod typescript_tests {
2416    use super::*;
2417
2418    #[test]
2419    fn copies_an_existing_annotation_into_the_new_signature() {
2420        let source = "function outer(base: number, scale: number) {\n  let acc = 0;\n  acc = base * scale;\n  return acc;\n}\n";
2421        let plan = plan_extraction(Lang::TypeScript, source.as_bytes(), 2, 2, "combine")
2422            .expect("planned");
2423        assert_eq!(
2424            plan.parameters,
2425            vec!["base".to_string(), "scale".to_string()]
2426        );
2427        assert_eq!(
2428            plan.parameter_spellings,
2429            vec!["base: number".to_string(), "scale: number".to_string()]
2430        );
2431        // `acc` was declared outside the range, so its declaration did not move
2432        // with the statements. Without a prologue the emitted body assigns a
2433        // name that is not in scope: it type-checks as `Cannot find name 'acc'`
2434        // and, in plain JS, silently creates a global.
2435        assert_eq!(plan.local_declarations, vec!["acc".to_string()]);
2436        let (function, call) = render_extraction(&plan, source, "combine");
2437        assert!(
2438            function.contains("function combine(base: number, scale: number) {"),
2439            "{function}"
2440        );
2441        assert!(
2442            function.contains("\n  let acc;\n  acc = base * scale;"),
2443            "{function}"
2444        );
2445        assert_eq!(call, "  acc = combine(base, scale);");
2446    }
2447
2448    #[test]
2449    fn refuses_a_parameter_whose_type_cannot_be_copied() {
2450        // The alternative is `unknown`, or nothing at all under
2451        // `noImplicitAny`. Both type-check something other than what the code
2452        // does, which is exactly the failure an extraction must not ship.
2453        let source =
2454            "function outer(base) {\n  let acc = 0;\n  acc = base * 2;\n  return acc;\n}\n";
2455        assert_eq!(
2456            plan_extraction(Lang::TypeScript, source.as_bytes(), 2, 2, "double"),
2457            Err(ExtractionRefusal::UnspellableParameterType(
2458                "base".to_string()
2459            ))
2460        );
2461    }
2462
2463    #[test]
2464    fn copies_a_generic_annotation_verbatim() {
2465        let source = "function outer(rows: Map<string, number>) {\n  let total = 0;\n  total = rows.size;\n  return total;\n}\n";
2466        let plan =
2467            plan_extraction(Lang::TypeScript, source.as_bytes(), 2, 2, "count").expect("planned");
2468        assert_eq!(
2469            plan.parameter_spellings,
2470            vec!["rows: Map<string, number>".to_string()]
2471        );
2472    }
2473}
2474
2475#[cfg(all(test, feature = "lang-rust"))]
2476mod rust_tests {
2477    use super::*;
2478
2479    // `rows` is moved in and never read again; `total` is threaded in and
2480    // handed back, which is what keeps the extraction by-value.
2481    const SOURCE: &str = "fn outer(rows: &[u32], limit: u32) -> u32 {\n    let mut total: u32 = 0;\n    for row in rows {\n        total += row * limit;\n    }\n    total\n}\n";
2482
2483    #[test]
2484    fn copies_annotations_and_threads_an_accumulator_by_value() {
2485        let plan = plan_extraction(Lang::Rust, SOURCE.as_bytes(), 2, 4, "accumulate")
2486            .expect("planned");
2487        assert_eq!(plan.enclosing_function, "outer");
2488        assert_eq!(
2489            plan.parameters,
2490            vec!["limit".to_string(), "rows".to_string(), "total".to_string()]
2491        );
2492        // `total` is assigned in the range, so the signature says `mut`; the
2493        // other two are read-only and do not.
2494        assert_eq!(
2495            plan.parameter_spellings,
2496            vec![
2497                "limit: u32".to_string(),
2498                "rows: &[u32]".to_string(),
2499                "mut total: u32".to_string()
2500            ]
2501        );
2502        assert_eq!(plan.returns, vec!["total".to_string()]);
2503        assert_eq!(plan.return_type, Some("u32".to_string()));
2504
2505        let (function, call) = render_extraction(&plan, SOURCE, "accumulate");
2506        assert!(
2507            function.contains("fn accumulate(limit: u32, rows: &[u32], mut total: u32) -> u32 {"),
2508            "{function}"
2509        );
2510        assert!(function.contains("\n    for row in rows {"), "{function}");
2511        // A trailing expression, not `return`.
2512        assert!(function.contains("\n    total\n"), "{function}");
2513        assert!(!function.contains("return"), "{function}");
2514        assert_eq!(call, "    total = accumulate(limit, rows, total);");
2515    }
2516
2517    #[test]
2518    fn refuses_a_name_it_would_move_and_the_caller_still_reads() {
2519        // `rows` is read after the range, so moving it in would leave the
2520        // caller reading a moved value. Borrowing instead would mean rewriting
2521        // every use in the body into a dereference.
2522        let source = "fn outer(rows: &[u32]) -> usize {\n    let mut total: usize = 0;\n    for row in rows {\n        total += *row as usize;\n    }\n    total + rows.len()\n}\n";
2523        assert_eq!(
2524            plan_extraction(Lang::Rust, source.as_bytes(), 2, 4, "accumulate"),
2525            Err(ExtractionRefusal::MovedNameUsedAfterRange("rows".to_string()))
2526        );
2527    }
2528
2529    #[test]
2530    fn refuses_an_unannotated_local() {
2531        // Idiomatic Rust rarely annotates a local, which is exactly why this
2532        // refuses rather than guessing: there is no type checker behind it,
2533        // and a guessed `T` parses and does not build.
2534        let source = "fn outer(base: u32) -> u32 {\n    let mut acc = 0;\n    acc += base;\n    acc\n}\n";
2535        assert_eq!(
2536            plan_extraction(Lang::Rust, source.as_bytes(), 2, 2, "bump"),
2537            Err(ExtractionRefusal::UnspellableParameterType("acc".to_string()))
2538        );
2539    }
2540
2541    #[test]
2542    fn refuses_the_trailing_expression() {
2543        // The tail *is* the function's return. Hoisting it would hand the
2544        // value to the new function and leave the caller returning nothing.
2545        assert_eq!(
2546            plan_extraction(Lang::Rust, SOURCE.as_bytes(), 5, 5, "finish"),
2547            Err(ExtractionRefusal::ReturnsThroughTailExpression)
2548        );
2549    }
2550
2551    #[test]
2552    fn refuses_the_question_mark_operator() {
2553        // `?` returns from the *enclosing* function. In a new function whose
2554        // return type is derived from names, there is nothing for it to return
2555        // through.
2556        let source = "fn outer(raw: &str) -> Result<u32, E> {\n    let n: u32 = parse(raw)?;\n    Ok(n)\n}\n";
2557        assert_eq!(
2558            plan_extraction(Lang::Rust, source.as_bytes(), 1, 1, "parsed"),
2559            Err(ExtractionRefusal::ControlFlowEscapes("?"))
2560        );
2561    }
2562
2563    #[test]
2564    fn refuses_an_await() {
2565        let source = "async fn outer(id: u32) -> u32 {\n    let n: u32 = fetch(id).await;\n    n\n}\n";
2566        assert_eq!(
2567            plan_extraction(Lang::Rust, source.as_bytes(), 1, 1, "fetched"),
2568            Err(ExtractionRefusal::ControlFlowEscapes(".await"))
2569        );
2570    }
2571
2572    #[test]
2573    fn refuses_a_method_body_that_names_self() {
2574        // Python threads `self` through the signature because it is an
2575        // ordinary name. Rust's is not: the new function would have to become
2576        // an inherent method, which needs an `impl` target and a receiver form
2577        // no derivation can choose without types.
2578        let source = "struct S { scale: u32 }\nimpl S {\n    fn outer(&self, base: u32) -> u32 {\n        let n: u32 = self.scale * base;\n        n\n    }\n}\n";
2579        assert_eq!(
2580            plan_extraction(Lang::Rust, source.as_bytes(), 3, 3, "scaled"),
2581            Err(ExtractionRefusal::ReferencesReceiver("self"))
2582        );
2583    }
2584
2585    #[test]
2586    fn a_method_extraction_without_self_lands_past_the_impl_block() {
2587        let source = "struct S;\nimpl S {\n    fn outer(&self, base: u32) -> u32 {\n        let n: u32 = base * 2;\n        n\n    }\n}\n";
2588        let plan =
2589            plan_extraction(Lang::Rust, source.as_bytes(), 3, 3, "double").expect("planned");
2590        assert_eq!(plan.enclosing_indent, "");
2591        assert_eq!(plan.insert_byte, source.len() - 1);
2592        let (function, call) = render_extraction(&plan, source, "double");
2593        assert!(function.contains("\nfn double(base: u32) -> u32 {"), "{function}");
2594        assert_eq!(call, "        let n = double(base);");
2595    }
2596}