1use crate::lang::Lang;
28use std::collections::BTreeSet;
29use tree_sitter::{Node, Parser};
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ExtractionPlan {
34 pub lang: Lang,
37 pub enclosing_function: String,
39 pub parameters: Vec<String>,
43 pub parameter_spellings: Vec<String>,
49 pub returns: Vec<String>,
51 pub return_type: Option<String>,
54 pub returns_declared_mut: bool,
57 pub local_declarations: Vec<String>,
62 pub returns_need_declaration: bool,
66 pub start_byte: usize,
68 pub end_byte: usize,
69 pub indent: String,
72 pub insert_byte: usize,
75 pub enclosing_indent: String,
77 pub indent_unit: String,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
89pub enum ExtractionRefusal {
90 UnsupportedLanguage(&'static str),
92 ParseFailed,
94 EmptyRange,
96 NotContiguousSiblings,
99 NotInsideFunction,
101 EnclosingFunctionNotHoistable,
106 ControlFlowEscapes(&'static str),
109 RebindsOuterScope(String),
112 NameCollision(String),
114 MultipleReturnsUnsupported(&'static str),
117 MixedReturnDeclarations,
121 MovedNameUsedAfterRange(String),
126 ReturnsThroughTailExpression,
130 ReferencesReceiver(&'static str),
134 AssignsUndeclaredName(String),
139 UnspellableParameterType(String),
143}
144
145impl ExtractionRefusal {
146 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
203struct Dialect {
204 family: Family,
205 annotates_parameters: bool,
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210enum Family {
211 Python,
213 GdScript,
215 JsLike,
217 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 Family::Rust => matches!(kind, "function_item" | "closure_expression"),
292 }
293 }
294
295 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 fn is_class_body_kind(self, kind: &str) -> bool {
321 match self.family {
322 Family::Python => false,
325 Family::GdScript | Family::JsLike => kind == "class_body",
326 Family::Rust => kind == "declaration_list",
327 }
328 }
329
330 fn hoists_out_of_class(self) -> bool {
338 !matches!(self.family, Family::GdScript)
339 }
340
341 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 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 (Family::Rust, "try_expression") => "?",
370 (Family::Rust, "await_expression") => ".await",
371 _ => return None,
375 };
376 Some(escape)
377 }
378
379 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 fn is_switch_kind(self, kind: &str) -> bool {
395 matches!(self.family, Family::JsLike) && kind == "switch_statement"
396 }
397
398 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 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 fn moves_parameters(self) -> bool {
443 matches!(self.family, Family::Rust)
444 }
445
446 fn annotates_mutability(self) -> bool {
448 matches!(self.family, Family::Rust)
449 }
450
451 fn annotates_return_type(self) -> bool {
454 matches!(self.family, Family::Rust)
455 }
456
457 fn max_returns(self) -> Option<usize> {
459 match self.family {
460 Family::GdScript => Some(1),
464 _ => None,
465 }
466 }
467}
468
469pub 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 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 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, ¶meters, &bound_outside_range)?;
569
570 if dialect.moves_parameters() {
576 for parameter in ¶meters {
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 ¶meters {
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
619pub 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 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
740fn rust_return_target(returns: &[String]) -> String {
743 if returns.len() == 1 {
744 returns[0].clone()
745 } else {
746 format!("({})", returns.join(", "))
747 }
748}
749
750fn 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
771fn 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 let mutable = if dialect.annotates_mutability() && plan.returns_declared_mut {
783 "mut "
784 } else {
785 ""
786 };
787 format!("{keyword} {mutable}")
788}
789
790fn js_return_target(returns: &[String]) -> String {
793 if returns.len() == 1 {
794 returns[0].clone()
795 } else {
796 format!("[{}]", returns.join(", "))
797 }
798}
799
800fn 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
819fn 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
853fn 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 Family::Rust => {
872 let prefix = if mutable { "mut " } else { "" };
873 format!("{prefix}{name}: {annotation}")
874 }
875 _ => format!("{name}{annotation}"),
876 })
877}
878
879fn 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
905fn 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
942fn 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
958fn 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 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 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
1025fn 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
1042fn 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 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 return Err(ExtractionRefusal::EnclosingFunctionNotHoistable);
1085 }
1086}
1087
1088fn 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 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 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
1157fn 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
1164fn 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
1172fn 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
1197fn 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
1223fn 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
1243fn 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
1265struct RangeBindings {
1272 declared: BTreeSet<String>,
1275 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 assigned.retain(|name| !declared.contains(name));
1306 RangeBindings { declared, assigned }
1307}
1308
1309fn 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 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 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
1387fn 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 if parent.is_some_and(|parent| is_augmented_assignment(dialect, parent.kind())) {
1408 events.push((node.start_byte(), false, name.clone()));
1409 }
1410 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 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
1468fn 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
1499fn 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
1523fn 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
1536fn 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 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
1552fn 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 "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
1634fn 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
1671fn 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 "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
1735fn 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 "attribute" => parent
1759 .child_by_field_name("attribute")
1760 .is_none_or(|attribute| attribute.id() != node.id()),
1761 "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 "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 if node.kind() == "name" {
1787 return false;
1788 }
1789 match parent.kind() {
1790 "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 "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
1836fn 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
1847fn 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 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 assert!(!plan.returns_need_declaration);
1887 }
1888
1889 #[test]
1890 fn a_name_the_range_assigns_first_is_a_local_not_a_parameter() {
1891 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 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 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 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 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 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 assert_eq!(
2024 plan.parameters,
2025 vec!["self".to_string(), "base".to_string()]
2026 );
2027 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}