1use std::collections::{BTreeSet, HashMap, HashSet};
19use std::path::Path;
20
21use crate::{PlannedEdit, RefactoringPlan};
22
23pub struct ExtractFunctionOutcome {
27 pub plan: RefactoringPlan,
29 pub function_name: String,
31 pub parameters: Vec<Parameter>,
33 pub return_type: ReturnType,
35 pub is_async: bool,
37 pub is_generator: bool,
39 pub call_site_line: u32,
42}
43
44#[derive(Debug, Clone)]
46pub struct Parameter {
47 pub name: String,
48 pub inferred_type: Option<String>,
50 pub mutable: bool,
52}
53
54#[derive(Debug, Clone)]
56pub enum ReturnType {
57 Unit,
58 Single(String),
59 Tuple(Vec<String>),
60 Result(String, String),
62}
63
64#[derive(Debug, Clone)]
66pub enum ExtractionWarning {
67 DeferCrossedBoundary {
68 line: u32,
69 },
70 ResourceLifetimeCrossedBoundary {
71 label: String,
72 acquire_line: u32,
73 },
74 ExceptionEscapesBoundary {
75 exception_type: String,
76 throw_line: u32,
77 },
78 MultipleLiveOutVariables {
79 names: Vec<String>,
80 },
81}
82
83impl ExtractionWarning {
84 pub fn to_string_lossy(&self) -> String {
85 match self {
86 ExtractionWarning::DeferCrossedBoundary { line } => {
87 format!(
88 "defer/deferred statement at line {} crosses extraction boundary; defer semantics may not transfer correctly",
89 line
90 )
91 }
92 ExtractionWarning::ResourceLifetimeCrossedBoundary {
93 label,
94 acquire_line,
95 } => {
96 format!(
97 "resource '{}' acquired at line {} but not released within extracted region; resource lifetime crosses extraction boundary",
98 label, acquire_line
99 )
100 }
101 ExtractionWarning::ExceptionEscapesBoundary {
102 exception_type,
103 throw_line,
104 } => {
105 format!(
106 "exception '{}' thrown at line {} escapes the extraction boundary; the extracted function must declare or handle it",
107 exception_type, throw_line
108 )
109 }
110 ExtractionWarning::MultipleLiveOutVariables { names } => {
111 format!(
112 "multiple live-out variables ({}); language may not support multiple return values — consider returning a struct",
113 names.join(", ")
114 )
115 }
116 }
117 }
118}
119
120#[derive(Debug)]
123struct BlockRow {
124 block_id: u32,
125 #[allow(dead_code)]
126 kind: String,
127 start_line: u32,
128 end_line: u32,
129}
130
131#[derive(Debug)]
132struct EdgeRow {
133 from: u32,
134 to: u32,
135 kind: String,
136 exception_type: Option<String>,
137}
138
139#[derive(Debug)]
140struct EffectRow {
141 block_id: u32,
142 kind: String,
143 line: u32,
144 label: Option<String>,
145}
146
147pub async fn plan_extract_function(
157 ctx: &crate::RefactoringContext,
158 file_abs: &Path,
159 content: &str,
160 start_line: u32,
161 end_line: u32,
162 function_name: &str,
163) -> Result<ExtractFunctionOutcome, String> {
164 let rel_path = file_abs
166 .strip_prefix(&ctx.root)
167 .unwrap_or(file_abs)
168 .to_string_lossy()
169 .to_string();
170
171 let grammar_name = normalize_languages::support_for_path(file_abs)
173 .map(|s| s.name().to_string())
174 .unwrap_or_default();
175
176 let index = ctx.index.as_ref().ok_or_else(|| {
178 "extract-function requires the facts index — run `normalize structure rebuild` first"
179 .to_string()
180 })?;
181
182 let conn = index.connection();
183
184 let func_row = find_enclosing_function(conn, &rel_path, start_line, end_line).await?;
186
187 let (func_qname, func_start_line) = match func_row {
188 Some(r) => r,
189 None => {
190 return Err(format!(
191 "no indexed function found containing lines {}-{} in {}; run `normalize structure rebuild`",
192 start_line, end_line, rel_path
193 ));
194 }
195 };
196
197 let block_rows = load_blocks(conn, &rel_path, &func_qname, func_start_line).await?;
199 let edge_rows = load_edges(conn, &rel_path, &func_qname, func_start_line).await?;
200 let def_rows = load_defs(conn, &rel_path, &func_qname, func_start_line).await?;
201 let use_rows = load_uses(conn, &rel_path, &func_qname, func_start_line).await?;
202 let effect_rows = load_effects(conn, &rel_path, &func_qname, func_start_line).await?;
203
204 let region_block_ids: HashSet<u32> = block_rows
206 .iter()
207 .filter(|b| b.start_line <= end_line && b.end_line >= start_line)
208 .map(|b| b.block_id)
209 .collect();
210
211 if region_block_ids.is_empty() {
212 return Err(format!(
213 "no CFG blocks found overlapping lines {}-{}; the region may be outside any statement",
214 start_line, end_line
215 ));
216 }
217
218 let block_ids: Vec<u32> = block_rows.iter().map(|b| b.block_id).collect();
220
221 let mut defs: HashMap<u32, BTreeSet<String>> = HashMap::new();
222 let mut uses_map: HashMap<u32, BTreeSet<String>> = HashMap::new();
223 for id in &block_ids {
224 defs.insert(*id, BTreeSet::new());
225 uses_map.insert(*id, BTreeSet::new());
226 }
227 for (bid, name) in &def_rows {
228 defs.entry(*bid).or_default().insert(name.clone());
229 }
230 for (bid, name) in &use_rows {
231 uses_map.entry(*bid).or_default().insert(name.clone());
232 }
233
234 let mut succs: HashMap<u32, Vec<u32>> = HashMap::new();
235 for id in &block_ids {
236 succs.insert(*id, Vec::new());
237 }
238 for e in &edge_rows {
239 succs.entry(e.from).or_default().push(e.to);
240 }
241
242 let (live_in, live_out) = compute_liveness(&block_ids, &defs, &uses_map, &succs);
243
244 let vars_defined_in_region: BTreeSet<String> = def_rows
246 .iter()
247 .filter(|(bid, _)| region_block_ids.contains(bid))
248 .map(|(_, name)| name.clone())
249 .collect();
250
251 let vars_defined_outside_region: BTreeSet<String> = def_rows
252 .iter()
253 .filter(|(bid, _)| !region_block_ids.contains(bid))
254 .map(|(_, name)| name.clone())
255 .collect();
256
257 let region_entry_blocks: Vec<u32> = region_block_ids
260 .iter()
261 .cloned()
262 .filter(|bid| {
263 !edge_rows
264 .iter()
265 .any(|e| e.to == *bid && region_block_ids.contains(&e.from))
266 })
267 .collect();
268
269 let mut param_names: BTreeSet<String> = BTreeSet::new();
270 for bid in ®ion_entry_blocks {
271 if let Some(li) = live_in.get(bid) {
272 for v in li {
273 if vars_defined_outside_region.contains(v) {
274 param_names.insert(v.clone());
275 }
276 }
277 }
278 }
279
280 let region_exit_blocks: Vec<u32> = region_block_ids
283 .iter()
284 .cloned()
285 .filter(|bid| {
286 succs
287 .get(bid)
288 .map(|ss| ss.iter().any(|s| !region_block_ids.contains(s)))
289 .unwrap_or(false)
290 })
291 .collect();
292
293 let mut return_var_names: BTreeSet<String> = BTreeSet::new();
294 for bid in ®ion_exit_blocks {
295 if let Some(lo) = live_out.get(bid) {
296 for v in lo {
297 if vars_defined_in_region.contains(v) {
298 return_var_names.insert(v.clone());
299 }
300 }
301 }
302 }
303
304 let parameters: Vec<Parameter> = param_names
308 .iter()
309 .map(|name| {
310 let mutable = is_mut_binding(&grammar_name, content, name);
311 let inferred_type = infer_type_from_annotation(&grammar_name, content, name);
312 Parameter {
313 name: name.clone(),
314 inferred_type,
315 mutable,
316 }
317 })
318 .collect();
319
320 let escaping_exceptions: Vec<(String, u32)> = edge_rows
323 .iter()
324 .filter(|e| {
325 e.kind == "exception"
326 && region_block_ids.contains(&e.from)
327 && !region_block_ids.contains(&e.to)
328 })
329 .filter_map(|e| e.exception_type.as_ref().map(|t| (t.clone(), e.from)))
330 .collect();
331
332 let return_type = if !escaping_exceptions.is_empty() && grammar_name == "rust" {
333 let ret_vars: Vec<String> = return_var_names.iter().cloned().collect();
334 let ok_type = if ret_vars.is_empty() {
335 "()".to_string()
336 } else if ret_vars.len() == 1 {
337 ret_vars[0].clone()
338 } else {
339 format!("({})", ret_vars.join(", "))
340 };
341 ReturnType::Result(ok_type, "Box<dyn std::error::Error>".to_string())
342 } else {
343 match return_var_names.len() {
344 0 => ReturnType::Unit,
345 1 => ReturnType::Single(return_var_names.iter().next().unwrap().clone()),
346 _ => {
347 let names: Vec<String> = return_var_names.iter().cloned().collect();
348 ReturnType::Tuple(names)
349 }
350 }
351 };
352
353 let region_effects: Vec<&EffectRow> = effect_rows
355 .iter()
356 .filter(|e| region_block_ids.contains(&e.block_id))
357 .collect();
358
359 let is_async = region_effects.iter().any(|e| e.kind == "await");
360 let is_generator = region_effects.iter().any(|e| e.kind == "yield");
361
362 let mut warnings: Vec<ExtractionWarning> = Vec::new();
364
365 for eff in ®ion_effects {
366 if eff.kind == "defer" {
367 warnings.push(ExtractionWarning::DeferCrossedBoundary { line: eff.line });
368 }
369 }
370
371 for eff in ®ion_effects {
373 if eff.kind == "acquire" {
374 let label = eff
375 .label
376 .clone()
377 .unwrap_or_else(|| "<resource>".to_string());
378 let has_release = region_effects
379 .iter()
380 .any(|e| e.kind == "release" && e.label == eff.label);
381 if !has_release {
382 warnings.push(ExtractionWarning::ResourceLifetimeCrossedBoundary {
383 label,
384 acquire_line: eff.line,
385 });
386 }
387 }
388 }
389
390 for (exc_type, from_bid) in &escaping_exceptions {
391 let throw_line = block_rows
393 .iter()
394 .find(|b| b.block_id == *from_bid)
395 .map(|b| b.start_line)
396 .unwrap_or(start_line);
397 warnings.push(ExtractionWarning::ExceptionEscapesBoundary {
398 exception_type: exc_type.clone(),
399 throw_line,
400 });
401 }
402
403 if let ReturnType::Tuple(ref names) = return_type {
404 if grammar_name != "go"
406 && grammar_name != "python"
407 && grammar_name != "typescript"
408 && grammar_name != "javascript"
409 {
410 warnings.push(ExtractionWarning::MultipleLiveOutVariables {
411 names: names.clone(),
412 });
413 }
414 }
415
416 let lines: Vec<&str> = content.lines().collect();
418 let region_start_idx = (start_line.saturating_sub(1)) as usize;
419 let region_end_idx = (end_line as usize).min(lines.len());
420
421 if region_start_idx >= lines.len() {
422 return Err(format!(
423 "start line {} is beyond end of file ({} lines)",
424 start_line,
425 lines.len()
426 ));
427 }
428
429 let region_lines: Vec<&str> = lines[region_start_idx..region_end_idx].to_vec();
430
431 let call_site_indent = region_lines
433 .iter()
434 .find(|l| !l.trim().is_empty())
435 .map(|l| {
436 let trimmed = l.trim_start();
437 &l[..l.len() - trimmed.len()]
438 })
439 .unwrap_or("");
440
441 let body_lines = strip_common_indent(®ion_lines);
443 let body_indent = " "; let new_function = generate_function(
447 &grammar_name,
448 function_name,
449 ¶meters,
450 &return_type,
451 is_async,
452 is_generator,
453 &body_lines,
454 body_indent,
455 );
456
457 let call_site = generate_call_site(
458 &grammar_name,
459 function_name,
460 ¶meters,
461 &return_type,
462 is_async,
463 call_site_indent,
464 );
465
466 let new_content = splice_content(content, start_line, end_line, &call_site, &new_function)?;
470
471 let plan = RefactoringPlan {
472 operation: "extract_function".to_string(),
473 edits: vec![PlannedEdit {
474 file: file_abs.to_path_buf(),
475 original: content.to_string(),
476 new_content,
477 description: format!("extract function '{}'", function_name),
478 }],
479 warnings: warnings.iter().map(|w| w.to_string_lossy()).collect(),
480 };
481
482 Ok(ExtractFunctionOutcome {
483 plan,
484 function_name: function_name.to_string(),
485 parameters,
486 return_type,
487 is_async,
488 is_generator,
489 call_site_line: start_line,
490 })
491}
492
493async fn find_enclosing_function(
496 conn: &libsql::Connection,
497 file: &str,
498 start_line: u32,
499 end_line: u32,
500) -> Result<Option<(String, u32)>, String> {
501 let mut rows = conn
504 .query(
505 "SELECT function_qname, function_start_line \
506 FROM cfg_blocks \
507 WHERE file = ?1 \
508 AND start_line <= ?2 \
509 AND end_line >= ?3 \
510 ORDER BY function_start_line DESC \
511 LIMIT 1",
512 libsql::params![file.to_string(), start_line as i64, end_line as i64],
513 )
514 .await
515 .map_err(|e| format!("DB error: {}", e))?;
516
517 match rows.next().await.map_err(|e| format!("DB error: {}", e))? {
518 Some(row) => {
519 let qname: String = row.get(0).map_err(|e| format!("DB error: {}", e))?;
520 let fsl: i64 = row.get(1).map_err(|e| format!("DB error: {}", e))?;
521 Ok(Some((qname, fsl as u32)))
522 }
523 None => Ok(None),
524 }
525}
526
527async fn load_blocks(
528 conn: &libsql::Connection,
529 file: &str,
530 func_qname: &str,
531 func_start_line: u32,
532) -> Result<Vec<BlockRow>, String> {
533 let mut rows = conn
534 .query(
535 "SELECT block_id, kind, start_line, end_line \
536 FROM cfg_blocks \
537 WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3 \
538 ORDER BY block_id",
539 libsql::params![
540 file.to_string(),
541 func_qname.to_string(),
542 func_start_line as i64
543 ],
544 )
545 .await
546 .map_err(|e| format!("DB error: {}", e))?;
547
548 let mut out = Vec::new();
549 while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
550 let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
551 let kind: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
552 let start_line: i64 = row.get(2).map_err(|e| format!("DB error: {}", e))?;
553 let end_line: i64 = row.get(3).map_err(|e| format!("DB error: {}", e))?;
554 out.push(BlockRow {
555 block_id: block_id as u32,
556 kind,
557 start_line: start_line as u32,
558 end_line: end_line as u32,
559 });
560 }
561 Ok(out)
562}
563
564async fn load_edges(
565 conn: &libsql::Connection,
566 file: &str,
567 func_qname: &str,
568 func_start_line: u32,
569) -> Result<Vec<EdgeRow>, String> {
570 let mut rows = conn
571 .query(
572 "SELECT from_block, to_block, kind, COALESCE(exception_type, '') \
573 FROM cfg_edges \
574 WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
575 libsql::params![
576 file.to_string(),
577 func_qname.to_string(),
578 func_start_line as i64
579 ],
580 )
581 .await
582 .map_err(|e| format!("DB error: {}", e))?;
583
584 let mut out = Vec::new();
585 while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
586 let from: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
587 let to: i64 = row.get(1).map_err(|e| format!("DB error: {}", e))?;
588 let kind: String = row.get(2).map_err(|e| format!("DB error: {}", e))?;
589 let exc: String = row.get(3).map_err(|e| format!("DB error: {}", e))?;
590 out.push(EdgeRow {
591 from: from as u32,
592 to: to as u32,
593 kind,
594 exception_type: if exc.is_empty() { None } else { Some(exc) },
595 });
596 }
597 Ok(out)
598}
599
600async fn load_defs(
601 conn: &libsql::Connection,
602 file: &str,
603 func_qname: &str,
604 func_start_line: u32,
605) -> Result<Vec<(u32, String)>, String> {
606 let mut rows = conn
607 .query(
608 "SELECT block_id, name \
609 FROM cfg_defs \
610 WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
611 libsql::params![
612 file.to_string(),
613 func_qname.to_string(),
614 func_start_line as i64
615 ],
616 )
617 .await
618 .map_err(|e| format!("DB error: {}", e))?;
619
620 let mut out = Vec::new();
621 while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
622 let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
623 let name: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
624 out.push((block_id as u32, name));
625 }
626 Ok(out)
627}
628
629async fn load_uses(
630 conn: &libsql::Connection,
631 file: &str,
632 func_qname: &str,
633 func_start_line: u32,
634) -> Result<Vec<(u32, String)>, String> {
635 let mut rows = conn
636 .query(
637 "SELECT block_id, name \
638 FROM cfg_uses \
639 WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
640 libsql::params![
641 file.to_string(),
642 func_qname.to_string(),
643 func_start_line as i64
644 ],
645 )
646 .await
647 .map_err(|e| format!("DB error: {}", e))?;
648
649 let mut out = Vec::new();
650 while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
651 let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
652 let name: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
653 out.push((block_id as u32, name));
654 }
655 Ok(out)
656}
657
658async fn load_effects(
659 conn: &libsql::Connection,
660 file: &str,
661 func_qname: &str,
662 func_start_line: u32,
663) -> Result<Vec<EffectRow>, String> {
664 let mut rows = conn
665 .query(
666 "SELECT block_id, kind, line, COALESCE(label, '') \
667 FROM cfg_effects \
668 WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
669 libsql::params![
670 file.to_string(),
671 func_qname.to_string(),
672 func_start_line as i64
673 ],
674 )
675 .await
676 .map_err(|e| format!("DB error: {}", e))?;
677
678 let mut out = Vec::new();
679 while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
680 let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
681 let kind: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
682 let line: i64 = row.get(2).map_err(|e| format!("DB error: {}", e))?;
683 let label: String = row.get(3).map_err(|e| format!("DB error: {}", e))?;
684 out.push(EffectRow {
685 block_id: block_id as u32,
686 kind,
687 line: line as u32,
688 label: if label.is_empty() { None } else { Some(label) },
689 });
690 }
691 Ok(out)
692}
693
694fn compute_liveness(
697 block_ids: &[u32],
698 defs: &HashMap<u32, BTreeSet<String>>,
699 uses_map: &HashMap<u32, BTreeSet<String>>,
700 succs: &HashMap<u32, Vec<u32>>,
701 ) -> (
703 HashMap<u32, BTreeSet<String>>,
704 HashMap<u32, BTreeSet<String>>,
705) {
706 let mut live_in: HashMap<u32, BTreeSet<String>> = HashMap::new();
707 let mut live_out: HashMap<u32, BTreeSet<String>> = HashMap::new();
708 for id in block_ids {
709 live_in.insert(*id, BTreeSet::new());
710 live_out.insert(*id, BTreeSet::new());
711 }
712
713 let empty: BTreeSet<String> = BTreeSet::new();
714 let mut changed = true;
715 while changed {
716 changed = false;
717 for &bid in block_ids.iter().rev() {
718 let mut new_lo: BTreeSet<String> = BTreeSet::new();
719 if let Some(succ_list) = succs.get(&bid) {
720 for &s in succ_list {
721 if let Some(li) = live_in.get(&s) {
722 new_lo.extend(li.iter().cloned());
723 }
724 }
725 }
726
727 let block_uses = uses_map.get(&bid).unwrap_or(&empty);
728 let block_defs = defs.get(&bid).unwrap_or(&empty);
729 let mut new_li: BTreeSet<String> = block_uses.clone();
730 for v in &new_lo {
731 if !block_defs.contains(v) {
732 new_li.insert(v.clone());
733 }
734 }
735
736 if new_lo != *live_out.get(&bid).unwrap_or(&empty)
737 || new_li != *live_in.get(&bid).unwrap_or(&empty)
738 {
739 changed = true;
740 live_out.insert(bid, new_lo);
741 live_in.insert(bid, new_li);
742 }
743 }
744 }
745
746 (live_in, live_out)
747}
748
749fn is_mut_binding(grammar: &str, content: &str, name: &str) -> bool {
753 if grammar != "rust" {
754 return false;
755 }
756 let pattern = format!("let mut {}", name);
759 content.contains(&pattern)
760}
761
762fn infer_type_from_annotation(grammar: &str, content: &str, name: &str) -> Option<String> {
765 match grammar {
766 "rust" => {
767 let pattern = format!("{}: ", name);
770 if let Some(pos) = content.find(&pattern) {
771 let after = &content[pos + pattern.len()..];
772 let end = after.find([',', ')', '=', '\n']).unwrap_or(after.len());
773 let ty = after[..end].trim().to_string();
774 if !ty.is_empty() && !ty.contains(' ') {
775 return Some(ty);
776 }
777 }
778 None
779 }
780 "typescript" => {
781 let pattern = format!("{}: ", name);
783 if let Some(pos) = content.find(&pattern) {
784 let after = &content[pos + pattern.len()..];
785 let end = after.find([',', ')', '=', '\n']).unwrap_or(after.len());
786 let ty = after[..end].trim().to_string();
787 if !ty.is_empty() {
788 return Some(ty);
789 }
790 }
791 None
792 }
793 _ => None,
794 }
795}
796
797#[allow(clippy::too_many_arguments)]
800fn generate_function(
801 grammar: &str,
802 name: &str,
803 params: &[Parameter],
804 ret: &ReturnType,
805 is_async: bool,
806 is_generator: bool,
807 body_lines: &[String],
808 indent: &str,
809) -> String {
810 match grammar {
811 "python" => generate_python_function(name, params, ret, is_async, body_lines, indent),
812 "go" => generate_go_function(name, params, ret, body_lines, indent),
813 "typescript" | "javascript" | "tsx" | "jsx" => {
814 generate_ts_function(grammar, name, params, ret, is_async, body_lines, indent)
815 }
816 "java" => generate_java_function(name, params, ret, body_lines, indent),
817 _ => {
818 generate_rust_function(
820 name,
821 params,
822 ret,
823 is_async,
824 is_generator,
825 body_lines,
826 indent,
827 )
828 }
829 }
830}
831
832fn generate_rust_function(
833 name: &str,
834 params: &[Parameter],
835 ret: &ReturnType,
836 is_async: bool,
837 _is_generator: bool,
838 body_lines: &[String],
839 indent: &str,
840) -> String {
841 let async_kw = if is_async { "async " } else { "" };
842 let param_str = params
843 .iter()
844 .map(|p| {
845 let mut_kw = if p.mutable { "mut " } else { "" };
846 match &p.inferred_type {
847 Some(ty) => format!("{}{}: {}", mut_kw, p.name, ty),
848 None => format!("{}{}: /* type */", mut_kw, p.name),
849 }
850 })
851 .collect::<Vec<_>>()
852 .join(", ");
853 let ret_str = match ret {
854 ReturnType::Unit => String::new(),
855 ReturnType::Single(v) => format!(" -> /* {} */", v),
856 ReturnType::Tuple(vs) => format!(" -> /* ({}) */", vs.join(", ")),
857 ReturnType::Result(ok, err) => format!(" -> Result</* {} */, {}>", ok, err),
858 };
859 let return_stmt = match ret {
860 ReturnType::Unit => String::new(),
861 ReturnType::Single(v) => format!("\n{} {}", indent, v),
862 ReturnType::Tuple(vs) => format!("\n{} ({})", indent, vs.join(", ")),
863 ReturnType::Result(ok, _) => {
864 if ok == "()" {
865 format!("\n{} Ok(())", indent)
866 } else {
867 format!("\n{} Ok({})", indent, ok)
868 }
869 }
870 };
871
872 let body = body_lines
873 .iter()
874 .map(|l| format!("{} {}", indent, l))
875 .collect::<Vec<_>>()
876 .join("\n");
877
878 format!(
879 "\n{}{}fn {}({}){} {{\n{}{}\n{}}}\n",
880 indent, async_kw, name, param_str, ret_str, body, return_stmt, indent
881 )
882}
883
884fn generate_python_function(
885 name: &str,
886 params: &[Parameter],
887 ret: &ReturnType,
888 is_async: bool,
889 body_lines: &[String],
890 indent: &str,
891) -> String {
892 let async_kw = if is_async { "async " } else { "" };
893 let param_str = params
894 .iter()
895 .map(|p| p.name.clone())
896 .collect::<Vec<_>>()
897 .join(", ");
898 let return_stmt = match ret {
899 ReturnType::Unit => String::new(),
900 ReturnType::Single(v) => format!("\n{} return {}", indent, v),
901 ReturnType::Tuple(vs) => format!("\n{} return {}", indent, vs.join(", ")),
902 ReturnType::Result(ok, _) => format!("\n{} return {}", indent, ok),
903 };
904
905 let body = body_lines
906 .iter()
907 .map(|l| format!("{} {}", indent, l))
908 .collect::<Vec<_>>()
909 .join("\n");
910
911 format!(
912 "\n{}{}def {}({}):\n{}{}\n",
913 indent, async_kw, name, param_str, body, return_stmt
914 )
915}
916
917fn generate_go_function(
918 name: &str,
919 params: &[Parameter],
920 ret: &ReturnType,
921 body_lines: &[String],
922 indent: &str,
923) -> String {
924 let param_str = params
926 .iter()
927 .map(|p| match &p.inferred_type {
928 Some(ty) => format!("{} {}", p.name, ty),
929 None => format!("{} interface{{}}", p.name),
930 })
931 .collect::<Vec<_>>()
932 .join(", ");
933 let ret_str = match ret {
934 ReturnType::Unit => String::new(),
935 ReturnType::Single(v) => format!(" /* {} */", v),
936 ReturnType::Tuple(vs) => format!(" ({} /* multi-return */)", vs.join(", ")),
937 ReturnType::Result(ok, _) => format!(" ({}, error)", ok),
938 };
939 let return_stmt = match ret {
940 ReturnType::Unit => String::new(),
941 ReturnType::Single(v) => format!("\n{} return {}", indent, v),
942 ReturnType::Tuple(vs) => format!("\n{} return {}", indent, vs.join(", ")),
943 ReturnType::Result(ok, _) => format!("\n{} return {}, nil", indent, ok),
944 };
945
946 let body = body_lines
947 .iter()
948 .map(|l| format!("{} {}", indent, l))
949 .collect::<Vec<_>>()
950 .join("\n");
951
952 format!(
953 "\n{}func {}({}){} {{\n{}{}\n{}}}\n",
954 indent, name, param_str, ret_str, body, return_stmt, indent
955 )
956}
957
958fn generate_ts_function(
959 grammar: &str,
960 name: &str,
961 params: &[Parameter],
962 ret: &ReturnType,
963 is_async: bool,
964 body_lines: &[String],
965 indent: &str,
966) -> String {
967 let async_kw = if is_async { "async " } else { "" };
968 let param_str = params
969 .iter()
970 .map(|p| match &p.inferred_type {
971 Some(ty) if grammar == "typescript" || grammar == "tsx" => {
972 format!("{}: {}", p.name, ty)
973 }
974 _ => p.name.clone(),
975 })
976 .collect::<Vec<_>>()
977 .join(", ");
978
979 let ret_annotation = if grammar == "typescript" || grammar == "tsx" {
980 match ret {
981 ReturnType::Unit => ": void".to_string(),
982 ReturnType::Single(v) => format!(": /* {} */", v),
983 ReturnType::Tuple(vs) => format!(
984 ": [{}]",
985 vs.iter()
986 .map(|v| format!("/* {} */", v))
987 .collect::<Vec<_>>()
988 .join(", ")
989 ),
990 ReturnType::Result(ok, _) => format!(": {} | Error", ok),
991 }
992 } else {
993 String::new()
994 };
995
996 let return_stmt = match ret {
997 ReturnType::Unit => String::new(),
998 ReturnType::Single(v) => format!("\n{} return {};", indent, v),
999 ReturnType::Tuple(vs) => format!("\n{} return [{}];", indent, vs.join(", ")),
1000 ReturnType::Result(ok, _) => format!("\n{} return {};", indent, ok),
1001 };
1002
1003 let body = body_lines
1004 .iter()
1005 .map(|l| format!("{} {}", indent, l))
1006 .collect::<Vec<_>>()
1007 .join("\n");
1008
1009 format!(
1010 "\n{}{}function {}({}){} {{\n{}{}\n{}}}\n",
1011 indent, async_kw, name, param_str, ret_annotation, body, return_stmt, indent
1012 )
1013}
1014
1015fn generate_java_function(
1016 name: &str,
1017 params: &[Parameter],
1018 ret: &ReturnType,
1019 body_lines: &[String],
1020 indent: &str,
1021) -> String {
1022 let ret_type = match ret {
1023 ReturnType::Unit => "void".to_string(),
1024 ReturnType::Single(v) => format!("/* {} */", v),
1025 ReturnType::Tuple(vs) => format!("/* TODO: struct({}) */", vs.join(", ")),
1026 ReturnType::Result(ok, err) => format!("/* {} throws {} */", ok, err),
1027 };
1028 let param_str = params
1029 .iter()
1030 .map(|p| match &p.inferred_type {
1031 Some(ty) => format!("{} {}", ty, p.name),
1032 None => format!("/* type */ {}", p.name),
1033 })
1034 .collect::<Vec<_>>()
1035 .join(", ");
1036 let return_stmt = match ret {
1037 ReturnType::Unit => String::new(),
1038 ReturnType::Single(v) => format!("\n{} return {};", indent, v),
1039 ReturnType::Tuple(vs) => {
1040 format!("\n{} // TODO: return struct({});", indent, vs.join(", "))
1041 }
1042 ReturnType::Result(ok, _) => format!("\n{} return {};", indent, ok),
1043 };
1044
1045 let body = body_lines
1046 .iter()
1047 .map(|l| format!("{} {}", indent, l))
1048 .collect::<Vec<_>>()
1049 .join("\n");
1050
1051 format!(
1052 "\n{}private {} {}({}) {{\n{}{}\n{}}}\n",
1053 indent, ret_type, name, param_str, body, return_stmt, indent
1054 )
1055}
1056
1057fn generate_call_site(
1058 grammar: &str,
1059 name: &str,
1060 params: &[Parameter],
1061 ret: &ReturnType,
1062 is_async: bool,
1063 indent: &str,
1064) -> String {
1065 let args = params
1066 .iter()
1067 .map(|p| p.name.as_str())
1068 .collect::<Vec<_>>()
1069 .join(", ");
1070 let await_kw = if is_async {
1071 match grammar {
1072 "rust" => ".await",
1073 _ => "await ",
1074 }
1075 } else {
1076 ""
1077 };
1078
1079 match grammar {
1080 "python" => match ret {
1081 ReturnType::Unit => format!(
1082 "{}{}{}{}",
1083 indent,
1084 await_kw,
1085 name,
1086 format_args!("({})", args)
1087 ),
1088 ReturnType::Single(v) => {
1089 format!("{}{} = {}{}({})\n", indent, v, await_kw, name, args)
1090 }
1091 ReturnType::Tuple(vs) => {
1092 format!(
1093 "{}{} = {}{}({})\n",
1094 indent,
1095 vs.join(", "),
1096 await_kw,
1097 name,
1098 args
1099 )
1100 }
1101 ReturnType::Result(ok, _) => format!("{}{} = {}({})\n", indent, ok, name, args),
1102 },
1103 "go" => match ret {
1104 ReturnType::Unit => format!("{}{}({})\n", indent, name, args),
1105 ReturnType::Single(v) => format!("{}{} := {}({})\n", indent, v, name, args),
1106 ReturnType::Tuple(vs) => {
1107 format!("{}{} := {}({})\n", indent, vs.join(", "), name, args)
1108 }
1109 ReturnType::Result(ok, _) => {
1110 format!(
1111 "{}{}, err := {}({})\n{}if err != nil {{ return err }}\n",
1112 indent, ok, name, args, indent
1113 )
1114 }
1115 },
1116 "typescript" | "javascript" | "tsx" | "jsx" => match ret {
1117 ReturnType::Unit => format!(
1118 "{}{}{}{}",
1119 indent,
1120 await_kw,
1121 name,
1122 format_args!("({});", args)
1123 ),
1124 ReturnType::Single(v) => {
1125 let prefix = if await_kw == "await " { "await " } else { "" };
1126 format!("{}const {} = {}{}({});\n", indent, v, prefix, name, args)
1127 }
1128 ReturnType::Tuple(vs) => {
1129 let prefix = if await_kw == "await " { "await " } else { "" };
1130 format!(
1131 "{}const [{}] = {}{}({});\n",
1132 indent,
1133 vs.join(", "),
1134 prefix,
1135 name,
1136 args
1137 )
1138 }
1139 ReturnType::Result(ok, _) => {
1140 let prefix = if await_kw == "await " { "await " } else { "" };
1141 format!("{}const {} = {}{}({});\n", indent, ok, prefix, name, args)
1142 }
1143 },
1144 "java" => match ret {
1145 ReturnType::Unit => format!("{}{}({});\n", indent, name, args),
1146 ReturnType::Single(v) => format!("{}var {} = {}({});\n", indent, v, name, args),
1147 ReturnType::Tuple(vs) => {
1148 format!(
1149 "{}var result = {}({}); // TODO: unpack ({})\n",
1150 indent,
1151 name,
1152 args,
1153 vs.join(", ")
1154 )
1155 }
1156 ReturnType::Result(ok, _) => format!("{}var {} = {}({});\n", indent, ok, name, args),
1157 },
1158 _ => {
1159 match ret {
1161 ReturnType::Unit => {
1162 if is_async {
1163 format!("{}{}({}).await;\n", indent, name, args)
1164 } else {
1165 format!("{}{}({});\n", indent, name, args)
1166 }
1167 }
1168 ReturnType::Single(v) => {
1169 if is_async {
1170 format!("{}let {} = {}({}).await;\n", indent, v, name, args)
1171 } else {
1172 format!("{}let {} = {}({});\n", indent, v, name, args)
1173 }
1174 }
1175 ReturnType::Tuple(vs) => {
1176 if is_async {
1177 format!(
1178 "{}let ({}) = {}({}).await;\n",
1179 indent,
1180 vs.join(", "),
1181 name,
1182 args
1183 )
1184 } else {
1185 format!("{}let ({}) = {}({});\n", indent, vs.join(", "), name, args)
1186 }
1187 }
1188 ReturnType::Result(ok, _) => {
1189 if is_async {
1190 format!("{}let {} = {}({}).await?;\n", indent, ok, name, args)
1191 } else {
1192 format!("{}let {} = {}({})?;\n", indent, ok, name, args)
1193 }
1194 }
1195 }
1196 }
1197 }
1198}
1199
1200fn strip_common_indent(lines: &[&str]) -> Vec<String> {
1204 let min_indent = lines
1205 .iter()
1206 .filter(|l| !l.trim().is_empty())
1207 .map(|l| l.len() - l.trim_start().len())
1208 .min()
1209 .unwrap_or(0);
1210
1211 lines
1212 .iter()
1213 .map(|l| {
1214 if l.len() >= min_indent {
1215 l[min_indent..].to_string()
1216 } else {
1217 l.to_string()
1218 }
1219 })
1220 .collect()
1221}
1222
1223fn splice_content(
1232 content: &str,
1233 start_line: u32,
1234 end_line: u32,
1235 call_site: &str,
1236 new_function: &str,
1237) -> Result<String, String> {
1238 let lines: Vec<&str> = content.lines().collect();
1239 let n = lines.len();
1240
1241 let start_idx = (start_line.saturating_sub(1)) as usize;
1242 let end_idx = (end_line as usize).min(n);
1243
1244 if start_idx >= n {
1245 return Err(format!(
1246 "start line {} is beyond end of file ({} lines)",
1247 start_line, n
1248 ));
1249 }
1250
1251 let mut new_lines: Vec<&str> = Vec::new();
1252 new_lines.extend_from_slice(&lines[..start_idx]);
1253
1254 let call_site_trimmed = call_site.trim_end_matches('\n');
1256 for l in call_site_trimmed.lines() {
1257 new_lines.push(l);
1258 }
1259
1260 new_lines.extend_from_slice(&lines[end_idx..]);
1261
1262 let had_trailing = content.ends_with('\n');
1264 let mut result = new_lines.join("\n");
1265 if had_trailing {
1266 result.push('\n');
1267 }
1268
1269 result.push_str(new_function);
1271
1272 Ok(result)
1273}
1274
1275pub fn parse_line_range(s: &str) -> Result<(u32, u32), String> {
1280 match s.split_once('-') {
1281 Some((a, b)) => {
1282 let start = a
1283 .trim()
1284 .parse::<u32>()
1285 .map_err(|_| format!("invalid start line in range '{}': expected integer", s))?;
1286 let end = b
1287 .trim()
1288 .parse::<u32>()
1289 .map_err(|_| format!("invalid end line in range '{}': expected integer", s))?;
1290 if start == 0 || end == 0 {
1291 return Err(format!(
1292 "line range '{}': lines are 1-based (must be ≥ 1)",
1293 s
1294 ));
1295 }
1296 if start > end {
1297 return Err(format!(
1298 "line range '{}': start ({}) must be ≤ end ({})",
1299 s, start, end
1300 ));
1301 }
1302 Ok((start, end))
1303 }
1304 None => Err(format!(
1305 "invalid line range '{}': expected 'start-end' (e.g. '10-25')",
1306 s
1307 )),
1308 }
1309}
1310
1311#[cfg(test)]
1314mod tests {
1315 use super::*;
1316
1317 #[test]
1320 fn parse_line_range_basic() {
1321 assert_eq!(parse_line_range("10-25").unwrap(), (10, 25));
1322 }
1323
1324 #[test]
1325 fn parse_line_range_single_line() {
1326 assert_eq!(parse_line_range("5-5").unwrap(), (5, 5));
1327 }
1328
1329 #[test]
1330 fn parse_line_range_rejects_zero() {
1331 assert!(parse_line_range("0-5").is_err());
1332 }
1333
1334 #[test]
1335 fn parse_line_range_rejects_inverted() {
1336 assert!(parse_line_range("10-5").is_err());
1337 }
1338
1339 #[test]
1340 fn parse_line_range_rejects_missing_dash() {
1341 assert!(parse_line_range("10").is_err());
1342 }
1343
1344 #[test]
1347 fn strip_common_indent_basic() {
1348 let lines = vec![" let x = 1;", " let y = 2;"];
1349 let out = strip_common_indent(&lines);
1350 assert_eq!(out, vec!["let x = 1;", "let y = 2;"]);
1351 }
1352
1353 #[test]
1354 fn strip_common_indent_mixed() {
1355 let lines = vec![" let x = 1;", " let y = 2;"];
1356 let out = strip_common_indent(&lines);
1357 assert_eq!(out, vec!["let x = 1;", " let y = 2;"]);
1358 }
1359
1360 #[test]
1363 fn generate_rust_fn_unit_return() {
1364 let params = vec![Parameter {
1365 name: "x".to_string(),
1366 inferred_type: Some("i32".to_string()),
1367 mutable: false,
1368 }];
1369 let body = vec!["println!(\"{}\", x);".to_string()];
1370 let out = generate_rust_function(
1371 "do_thing",
1372 ¶ms,
1373 &ReturnType::Unit,
1374 false,
1375 false,
1376 &body,
1377 "",
1378 );
1379 assert!(out.contains("fn do_thing(x: i32)"));
1380 assert!(out.contains("println!"));
1381 assert!(!out.contains("->"));
1382 }
1383
1384 #[test]
1385 fn generate_rust_fn_single_return() {
1386 let params = vec![];
1387 let body = vec!["let result = 42;".to_string()];
1388 let out = generate_rust_function(
1389 "compute",
1390 ¶ms,
1391 &ReturnType::Single("result".to_string()),
1392 false,
1393 false,
1394 &body,
1395 "",
1396 );
1397 assert!(out.contains("fn compute()"));
1398 assert!(out.contains("-> /* result */"));
1399 assert!(out.contains("result"));
1400 }
1401
1402 #[test]
1403 fn generate_rust_fn_async() {
1404 let params = vec![];
1405 let body = vec!["tokio::time::sleep(Duration::from_secs(1)).await;".to_string()];
1406 let out = generate_rust_function(
1407 "wait_a_bit",
1408 ¶ms,
1409 &ReturnType::Unit,
1410 true,
1411 false,
1412 &body,
1413 "",
1414 );
1415 assert!(out.contains("async fn wait_a_bit()"));
1416 }
1417
1418 #[test]
1421 fn generate_python_fn_basic() {
1422 let params = vec![Parameter {
1423 name: "x".to_string(),
1424 inferred_type: None,
1425 mutable: false,
1426 }];
1427 let body = vec!["print(x)".to_string()];
1428 let out = generate_python_function("show", ¶ms, &ReturnType::Unit, false, &body, "");
1429 assert!(out.contains("def show(x):"));
1430 assert!(out.contains("print(x)"));
1431 }
1432
1433 #[test]
1434 fn generate_python_fn_multi_return() {
1435 let params = vec![];
1436 let body = vec!["a = 1".to_string(), "b = 2".to_string()];
1437 let out = generate_python_function(
1438 "two_values",
1439 ¶ms,
1440 &ReturnType::Tuple(vec!["a".to_string(), "b".to_string()]),
1441 false,
1442 &body,
1443 "",
1444 );
1445 assert!(out.contains("return a, b"));
1446 }
1447
1448 #[test]
1451 fn generate_go_fn_basic() {
1452 let params = vec![Parameter {
1453 name: "n".to_string(),
1454 inferred_type: Some("int".to_string()),
1455 mutable: false,
1456 }];
1457 let body = vec!["result := n * 2".to_string()];
1458 let out = generate_go_function(
1459 "double",
1460 ¶ms,
1461 &ReturnType::Single("result".to_string()),
1462 &body,
1463 "",
1464 );
1465 assert!(out.contains("func double(n int)"));
1466 assert!(out.contains("return result"));
1467 }
1468
1469 #[test]
1472 fn splice_content_replaces_region() {
1473 let content = "line1\nline2\nline3\nline4\nline5\n";
1474 let result =
1475 splice_content(content, 2, 3, " call()\n", "\nfn extracted() {}\n").unwrap();
1476 assert!(result.contains("line1\n call()\nline4\nline5\n"));
1477 assert!(result.contains("fn extracted()"));
1478 }
1479
1480 #[test]
1481 fn splice_content_preserves_surrounding_lines() {
1482 let content = "a\nb\nc\nd\n";
1483 let result = splice_content(content, 2, 2, "X\n", "\nfn f() {}\n").unwrap();
1484 assert!(result.starts_with("a\nX\nc\nd\n"));
1485 }
1486
1487 #[test]
1490 fn rust_call_site_with_return() {
1491 let params = vec![Parameter {
1492 name: "x".to_string(),
1493 inferred_type: Some("i32".to_string()),
1494 mutable: false,
1495 }];
1496 let call = generate_call_site(
1497 "rust",
1498 "compute",
1499 ¶ms,
1500 &ReturnType::Single("result".to_string()),
1501 false,
1502 " ",
1503 );
1504 assert_eq!(call, " let result = compute(x);\n");
1505 }
1506
1507 #[test]
1508 fn rust_call_site_async() {
1509 let params = vec![];
1510 let call = generate_call_site(
1511 "rust",
1512 "fetch",
1513 ¶ms,
1514 &ReturnType::Single("data".to_string()),
1515 true,
1516 " ",
1517 );
1518 assert_eq!(call, " let data = fetch().await;\n");
1519 }
1520
1521 #[test]
1522 fn python_call_site_multi_return() {
1523 let params = vec![];
1524 let call = generate_call_site(
1525 "python",
1526 "two_vals",
1527 ¶ms,
1528 &ReturnType::Tuple(vec!["a".to_string(), "b".to_string()]),
1529 false,
1530 "",
1531 );
1532 assert_eq!(call, "a, b = two_vals()\n");
1533 }
1534}