use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::Path;
use crate::{PlannedEdit, RefactoringPlan};
pub struct ExtractFunctionOutcome {
pub plan: RefactoringPlan,
pub function_name: String,
pub parameters: Vec<Parameter>,
pub return_type: ReturnType,
pub is_async: bool,
pub is_generator: bool,
pub call_site_line: u32,
}
#[derive(Debug, Clone)]
pub struct Parameter {
pub name: String,
pub inferred_type: Option<String>,
pub mutable: bool,
}
#[derive(Debug, Clone)]
pub enum ReturnType {
Unit,
Single(String),
Tuple(Vec<String>),
Result(String, String),
}
#[derive(Debug, Clone)]
pub enum ExtractionWarning {
DeferCrossedBoundary {
line: u32,
},
ResourceLifetimeCrossedBoundary {
label: String,
acquire_line: u32,
},
ExceptionEscapesBoundary {
exception_type: String,
throw_line: u32,
},
MultipleLiveOutVariables {
names: Vec<String>,
},
}
impl ExtractionWarning {
pub fn to_string_lossy(&self) -> String {
match self {
ExtractionWarning::DeferCrossedBoundary { line } => {
format!(
"defer/deferred statement at line {} crosses extraction boundary; defer semantics may not transfer correctly",
line
)
}
ExtractionWarning::ResourceLifetimeCrossedBoundary {
label,
acquire_line,
} => {
format!(
"resource '{}' acquired at line {} but not released within extracted region; resource lifetime crosses extraction boundary",
label, acquire_line
)
}
ExtractionWarning::ExceptionEscapesBoundary {
exception_type,
throw_line,
} => {
format!(
"exception '{}' thrown at line {} escapes the extraction boundary; the extracted function must declare or handle it",
exception_type, throw_line
)
}
ExtractionWarning::MultipleLiveOutVariables { names } => {
format!(
"multiple live-out variables ({}); language may not support multiple return values — consider returning a struct",
names.join(", ")
)
}
}
}
}
#[derive(Debug)]
struct BlockRow {
block_id: u32,
#[allow(dead_code)]
kind: String,
start_line: u32,
end_line: u32,
}
#[derive(Debug)]
struct EdgeRow {
from: u32,
to: u32,
kind: String,
exception_type: Option<String>,
}
#[derive(Debug)]
struct EffectRow {
block_id: u32,
kind: String,
line: u32,
label: Option<String>,
}
pub async fn plan_extract_function(
ctx: &crate::RefactoringContext,
file_abs: &Path,
content: &str,
start_line: u32,
end_line: u32,
function_name: &str,
) -> Result<ExtractFunctionOutcome, String> {
let rel_path = file_abs
.strip_prefix(&ctx.root)
.unwrap_or(file_abs)
.to_string_lossy()
.to_string();
let grammar_name = normalize_languages::support_for_path(file_abs)
.map(|s| s.name().to_string())
.unwrap_or_default();
let index = ctx.index.as_ref().ok_or_else(|| {
"extract-function requires the facts index — run `normalize structure rebuild` first"
.to_string()
})?;
let conn = index.connection();
let func_row = find_enclosing_function(conn, &rel_path, start_line, end_line).await?;
let (func_qname, func_start_line) = match func_row {
Some(r) => r,
None => {
return Err(format!(
"no indexed function found containing lines {}-{} in {}; run `normalize structure rebuild`",
start_line, end_line, rel_path
));
}
};
let block_rows = load_blocks(conn, &rel_path, &func_qname, func_start_line).await?;
let edge_rows = load_edges(conn, &rel_path, &func_qname, func_start_line).await?;
let def_rows = load_defs(conn, &rel_path, &func_qname, func_start_line).await?;
let use_rows = load_uses(conn, &rel_path, &func_qname, func_start_line).await?;
let effect_rows = load_effects(conn, &rel_path, &func_qname, func_start_line).await?;
let region_block_ids: HashSet<u32> = block_rows
.iter()
.filter(|b| b.start_line <= end_line && b.end_line >= start_line)
.map(|b| b.block_id)
.collect();
if region_block_ids.is_empty() {
return Err(format!(
"no CFG blocks found overlapping lines {}-{}; the region may be outside any statement",
start_line, end_line
));
}
let block_ids: Vec<u32> = block_rows.iter().map(|b| b.block_id).collect();
let mut defs: HashMap<u32, BTreeSet<String>> = HashMap::new();
let mut uses_map: HashMap<u32, BTreeSet<String>> = HashMap::new();
for id in &block_ids {
defs.insert(*id, BTreeSet::new());
uses_map.insert(*id, BTreeSet::new());
}
for (bid, name) in &def_rows {
defs.entry(*bid).or_default().insert(name.clone());
}
for (bid, name) in &use_rows {
uses_map.entry(*bid).or_default().insert(name.clone());
}
let mut succs: HashMap<u32, Vec<u32>> = HashMap::new();
for id in &block_ids {
succs.insert(*id, Vec::new());
}
for e in &edge_rows {
succs.entry(e.from).or_default().push(e.to);
}
let (live_in, live_out) = compute_liveness(&block_ids, &defs, &uses_map, &succs);
let vars_defined_in_region: BTreeSet<String> = def_rows
.iter()
.filter(|(bid, _)| region_block_ids.contains(bid))
.map(|(_, name)| name.clone())
.collect();
let vars_defined_outside_region: BTreeSet<String> = def_rows
.iter()
.filter(|(bid, _)| !region_block_ids.contains(bid))
.map(|(_, name)| name.clone())
.collect();
let region_entry_blocks: Vec<u32> = region_block_ids
.iter()
.cloned()
.filter(|bid| {
!edge_rows
.iter()
.any(|e| e.to == *bid && region_block_ids.contains(&e.from))
})
.collect();
let mut param_names: BTreeSet<String> = BTreeSet::new();
for bid in ®ion_entry_blocks {
if let Some(li) = live_in.get(bid) {
for v in li {
if vars_defined_outside_region.contains(v) {
param_names.insert(v.clone());
}
}
}
}
let region_exit_blocks: Vec<u32> = region_block_ids
.iter()
.cloned()
.filter(|bid| {
succs
.get(bid)
.map(|ss| ss.iter().any(|s| !region_block_ids.contains(s)))
.unwrap_or(false)
})
.collect();
let mut return_var_names: BTreeSet<String> = BTreeSet::new();
for bid in ®ion_exit_blocks {
if let Some(lo) = live_out.get(bid) {
for v in lo {
if vars_defined_in_region.contains(v) {
return_var_names.insert(v.clone());
}
}
}
}
let parameters: Vec<Parameter> = param_names
.iter()
.map(|name| {
let mutable = is_mut_binding(&grammar_name, content, name);
let inferred_type = infer_type_from_annotation(&grammar_name, content, name);
Parameter {
name: name.clone(),
inferred_type,
mutable,
}
})
.collect();
let escaping_exceptions: Vec<(String, u32)> = edge_rows
.iter()
.filter(|e| {
e.kind == "exception"
&& region_block_ids.contains(&e.from)
&& !region_block_ids.contains(&e.to)
})
.filter_map(|e| e.exception_type.as_ref().map(|t| (t.clone(), e.from)))
.collect();
let return_type = if !escaping_exceptions.is_empty() && grammar_name == "rust" {
let ret_vars: Vec<String> = return_var_names.iter().cloned().collect();
let ok_type = if ret_vars.is_empty() {
"()".to_string()
} else if ret_vars.len() == 1 {
ret_vars[0].clone()
} else {
format!("({})", ret_vars.join(", "))
};
ReturnType::Result(ok_type, "Box<dyn std::error::Error>".to_string())
} else {
match return_var_names.len() {
0 => ReturnType::Unit,
1 => ReturnType::Single(return_var_names.iter().next().unwrap().clone()),
_ => {
let names: Vec<String> = return_var_names.iter().cloned().collect();
ReturnType::Tuple(names)
}
}
};
let region_effects: Vec<&EffectRow> = effect_rows
.iter()
.filter(|e| region_block_ids.contains(&e.block_id))
.collect();
let is_async = region_effects.iter().any(|e| e.kind == "await");
let is_generator = region_effects.iter().any(|e| e.kind == "yield");
let mut warnings: Vec<ExtractionWarning> = Vec::new();
for eff in ®ion_effects {
if eff.kind == "defer" {
warnings.push(ExtractionWarning::DeferCrossedBoundary { line: eff.line });
}
}
for eff in ®ion_effects {
if eff.kind == "acquire" {
let label = eff
.label
.clone()
.unwrap_or_else(|| "<resource>".to_string());
let has_release = region_effects
.iter()
.any(|e| e.kind == "release" && e.label == eff.label);
if !has_release {
warnings.push(ExtractionWarning::ResourceLifetimeCrossedBoundary {
label,
acquire_line: eff.line,
});
}
}
}
for (exc_type, from_bid) in &escaping_exceptions {
let throw_line = block_rows
.iter()
.find(|b| b.block_id == *from_bid)
.map(|b| b.start_line)
.unwrap_or(start_line);
warnings.push(ExtractionWarning::ExceptionEscapesBoundary {
exception_type: exc_type.clone(),
throw_line,
});
}
if let ReturnType::Tuple(ref names) = return_type {
if grammar_name != "go"
&& grammar_name != "python"
&& grammar_name != "typescript"
&& grammar_name != "javascript"
{
warnings.push(ExtractionWarning::MultipleLiveOutVariables {
names: names.clone(),
});
}
}
let lines: Vec<&str> = content.lines().collect();
let region_start_idx = (start_line.saturating_sub(1)) as usize;
let region_end_idx = (end_line as usize).min(lines.len());
if region_start_idx >= lines.len() {
return Err(format!(
"start line {} is beyond end of file ({} lines)",
start_line,
lines.len()
));
}
let region_lines: Vec<&str> = lines[region_start_idx..region_end_idx].to_vec();
let call_site_indent = region_lines
.iter()
.find(|l| !l.trim().is_empty())
.map(|l| {
let trimmed = l.trim_start();
&l[..l.len() - trimmed.len()]
})
.unwrap_or("");
let body_lines = strip_common_indent(®ion_lines);
let body_indent = " ";
let new_function = generate_function(
&grammar_name,
function_name,
¶meters,
&return_type,
is_async,
is_generator,
&body_lines,
body_indent,
);
let call_site = generate_call_site(
&grammar_name,
function_name,
¶meters,
&return_type,
is_async,
call_site_indent,
);
let new_content = splice_content(content, start_line, end_line, &call_site, &new_function)?;
let plan = RefactoringPlan {
operation: "extract_function".to_string(),
edits: vec![PlannedEdit {
file: file_abs.to_path_buf(),
original: content.to_string(),
new_content,
description: format!("extract function '{}'", function_name),
}],
warnings: warnings.iter().map(|w| w.to_string_lossy()).collect(),
};
Ok(ExtractFunctionOutcome {
plan,
function_name: function_name.to_string(),
parameters,
return_type,
is_async,
is_generator,
call_site_line: start_line,
})
}
async fn find_enclosing_function(
conn: &libsql::Connection,
file: &str,
start_line: u32,
end_line: u32,
) -> Result<Option<(String, u32)>, String> {
let mut rows = conn
.query(
"SELECT function_qname, function_start_line \
FROM cfg_blocks \
WHERE file = ?1 \
AND start_line <= ?2 \
AND end_line >= ?3 \
ORDER BY function_start_line DESC \
LIMIT 1",
libsql::params![file.to_string(), start_line as i64, end_line as i64],
)
.await
.map_err(|e| format!("DB error: {}", e))?;
match rows.next().await.map_err(|e| format!("DB error: {}", e))? {
Some(row) => {
let qname: String = row.get(0).map_err(|e| format!("DB error: {}", e))?;
let fsl: i64 = row.get(1).map_err(|e| format!("DB error: {}", e))?;
Ok(Some((qname, fsl as u32)))
}
None => Ok(None),
}
}
async fn load_blocks(
conn: &libsql::Connection,
file: &str,
func_qname: &str,
func_start_line: u32,
) -> Result<Vec<BlockRow>, String> {
let mut rows = conn
.query(
"SELECT block_id, kind, start_line, end_line \
FROM cfg_blocks \
WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3 \
ORDER BY block_id",
libsql::params![
file.to_string(),
func_qname.to_string(),
func_start_line as i64
],
)
.await
.map_err(|e| format!("DB error: {}", e))?;
let mut out = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
let kind: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
let start_line: i64 = row.get(2).map_err(|e| format!("DB error: {}", e))?;
let end_line: i64 = row.get(3).map_err(|e| format!("DB error: {}", e))?;
out.push(BlockRow {
block_id: block_id as u32,
kind,
start_line: start_line as u32,
end_line: end_line as u32,
});
}
Ok(out)
}
async fn load_edges(
conn: &libsql::Connection,
file: &str,
func_qname: &str,
func_start_line: u32,
) -> Result<Vec<EdgeRow>, String> {
let mut rows = conn
.query(
"SELECT from_block, to_block, kind, COALESCE(exception_type, '') \
FROM cfg_edges \
WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
libsql::params![
file.to_string(),
func_qname.to_string(),
func_start_line as i64
],
)
.await
.map_err(|e| format!("DB error: {}", e))?;
let mut out = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
let from: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
let to: i64 = row.get(1).map_err(|e| format!("DB error: {}", e))?;
let kind: String = row.get(2).map_err(|e| format!("DB error: {}", e))?;
let exc: String = row.get(3).map_err(|e| format!("DB error: {}", e))?;
out.push(EdgeRow {
from: from as u32,
to: to as u32,
kind,
exception_type: if exc.is_empty() { None } else { Some(exc) },
});
}
Ok(out)
}
async fn load_defs(
conn: &libsql::Connection,
file: &str,
func_qname: &str,
func_start_line: u32,
) -> Result<Vec<(u32, String)>, String> {
let mut rows = conn
.query(
"SELECT block_id, name \
FROM cfg_defs \
WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
libsql::params![
file.to_string(),
func_qname.to_string(),
func_start_line as i64
],
)
.await
.map_err(|e| format!("DB error: {}", e))?;
let mut out = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
let name: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
out.push((block_id as u32, name));
}
Ok(out)
}
async fn load_uses(
conn: &libsql::Connection,
file: &str,
func_qname: &str,
func_start_line: u32,
) -> Result<Vec<(u32, String)>, String> {
let mut rows = conn
.query(
"SELECT block_id, name \
FROM cfg_uses \
WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
libsql::params![
file.to_string(),
func_qname.to_string(),
func_start_line as i64
],
)
.await
.map_err(|e| format!("DB error: {}", e))?;
let mut out = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
let name: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
out.push((block_id as u32, name));
}
Ok(out)
}
async fn load_effects(
conn: &libsql::Connection,
file: &str,
func_qname: &str,
func_start_line: u32,
) -> Result<Vec<EffectRow>, String> {
let mut rows = conn
.query(
"SELECT block_id, kind, line, COALESCE(label, '') \
FROM cfg_effects \
WHERE file = ?1 AND function_qname = ?2 AND function_start_line = ?3",
libsql::params![
file.to_string(),
func_qname.to_string(),
func_start_line as i64
],
)
.await
.map_err(|e| format!("DB error: {}", e))?;
let mut out = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| format!("DB error: {}", e))? {
let block_id: i64 = row.get(0).map_err(|e| format!("DB error: {}", e))?;
let kind: String = row.get(1).map_err(|e| format!("DB error: {}", e))?;
let line: i64 = row.get(2).map_err(|e| format!("DB error: {}", e))?;
let label: String = row.get(3).map_err(|e| format!("DB error: {}", e))?;
out.push(EffectRow {
block_id: block_id as u32,
kind,
line: line as u32,
label: if label.is_empty() { None } else { Some(label) },
});
}
Ok(out)
}
fn compute_liveness(
block_ids: &[u32],
defs: &HashMap<u32, BTreeSet<String>>,
uses_map: &HashMap<u32, BTreeSet<String>>,
succs: &HashMap<u32, Vec<u32>>,
) -> (
HashMap<u32, BTreeSet<String>>,
HashMap<u32, BTreeSet<String>>,
) {
let mut live_in: HashMap<u32, BTreeSet<String>> = HashMap::new();
let mut live_out: HashMap<u32, BTreeSet<String>> = HashMap::new();
for id in block_ids {
live_in.insert(*id, BTreeSet::new());
live_out.insert(*id, BTreeSet::new());
}
let empty: BTreeSet<String> = BTreeSet::new();
let mut changed = true;
while changed {
changed = false;
for &bid in block_ids.iter().rev() {
let mut new_lo: BTreeSet<String> = BTreeSet::new();
if let Some(succ_list) = succs.get(&bid) {
for &s in succ_list {
if let Some(li) = live_in.get(&s) {
new_lo.extend(li.iter().cloned());
}
}
}
let block_uses = uses_map.get(&bid).unwrap_or(&empty);
let block_defs = defs.get(&bid).unwrap_or(&empty);
let mut new_li: BTreeSet<String> = block_uses.clone();
for v in &new_lo {
if !block_defs.contains(v) {
new_li.insert(v.clone());
}
}
if new_lo != *live_out.get(&bid).unwrap_or(&empty)
|| new_li != *live_in.get(&bid).unwrap_or(&empty)
{
changed = true;
live_out.insert(bid, new_lo);
live_in.insert(bid, new_li);
}
}
}
(live_in, live_out)
}
fn is_mut_binding(grammar: &str, content: &str, name: &str) -> bool {
if grammar != "rust" {
return false;
}
let pattern = format!("let mut {}", name);
content.contains(&pattern)
}
fn infer_type_from_annotation(grammar: &str, content: &str, name: &str) -> Option<String> {
match grammar {
"rust" => {
let pattern = format!("{}: ", name);
if let Some(pos) = content.find(&pattern) {
let after = &content[pos + pattern.len()..];
let end = after.find([',', ')', '=', '\n']).unwrap_or(after.len());
let ty = after[..end].trim().to_string();
if !ty.is_empty() && !ty.contains(' ') {
return Some(ty);
}
}
None
}
"typescript" => {
let pattern = format!("{}: ", name);
if let Some(pos) = content.find(&pattern) {
let after = &content[pos + pattern.len()..];
let end = after.find([',', ')', '=', '\n']).unwrap_or(after.len());
let ty = after[..end].trim().to_string();
if !ty.is_empty() {
return Some(ty);
}
}
None
}
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
fn generate_function(
grammar: &str,
name: &str,
params: &[Parameter],
ret: &ReturnType,
is_async: bool,
is_generator: bool,
body_lines: &[String],
indent: &str,
) -> String {
match grammar {
"python" => generate_python_function(name, params, ret, is_async, body_lines, indent),
"go" => generate_go_function(name, params, ret, body_lines, indent),
"typescript" | "javascript" | "tsx" | "jsx" => {
generate_ts_function(grammar, name, params, ret, is_async, body_lines, indent)
}
"java" => generate_java_function(name, params, ret, body_lines, indent),
_ => {
generate_rust_function(
name,
params,
ret,
is_async,
is_generator,
body_lines,
indent,
)
}
}
}
fn generate_rust_function(
name: &str,
params: &[Parameter],
ret: &ReturnType,
is_async: bool,
_is_generator: bool,
body_lines: &[String],
indent: &str,
) -> String {
let async_kw = if is_async { "async " } else { "" };
let param_str = params
.iter()
.map(|p| {
let mut_kw = if p.mutable { "mut " } else { "" };
match &p.inferred_type {
Some(ty) => format!("{}{}: {}", mut_kw, p.name, ty),
None => format!("{}{}: /* type */", mut_kw, p.name),
}
})
.collect::<Vec<_>>()
.join(", ");
let ret_str = match ret {
ReturnType::Unit => String::new(),
ReturnType::Single(v) => format!(" -> /* {} */", v),
ReturnType::Tuple(vs) => format!(" -> /* ({}) */", vs.join(", ")),
ReturnType::Result(ok, err) => format!(" -> Result</* {} */, {}>", ok, err),
};
let return_stmt = match ret {
ReturnType::Unit => String::new(),
ReturnType::Single(v) => format!("\n{} {}", indent, v),
ReturnType::Tuple(vs) => format!("\n{} ({})", indent, vs.join(", ")),
ReturnType::Result(ok, _) => {
if ok == "()" {
format!("\n{} Ok(())", indent)
} else {
format!("\n{} Ok({})", indent, ok)
}
}
};
let body = body_lines
.iter()
.map(|l| format!("{} {}", indent, l))
.collect::<Vec<_>>()
.join("\n");
format!(
"\n{}{}fn {}({}){} {{\n{}{}\n{}}}\n",
indent, async_kw, name, param_str, ret_str, body, return_stmt, indent
)
}
fn generate_python_function(
name: &str,
params: &[Parameter],
ret: &ReturnType,
is_async: bool,
body_lines: &[String],
indent: &str,
) -> String {
let async_kw = if is_async { "async " } else { "" };
let param_str = params
.iter()
.map(|p| p.name.clone())
.collect::<Vec<_>>()
.join(", ");
let return_stmt = match ret {
ReturnType::Unit => String::new(),
ReturnType::Single(v) => format!("\n{} return {}", indent, v),
ReturnType::Tuple(vs) => format!("\n{} return {}", indent, vs.join(", ")),
ReturnType::Result(ok, _) => format!("\n{} return {}", indent, ok),
};
let body = body_lines
.iter()
.map(|l| format!("{} {}", indent, l))
.collect::<Vec<_>>()
.join("\n");
format!(
"\n{}{}def {}({}):\n{}{}\n",
indent, async_kw, name, param_str, body, return_stmt
)
}
fn generate_go_function(
name: &str,
params: &[Parameter],
ret: &ReturnType,
body_lines: &[String],
indent: &str,
) -> String {
let param_str = params
.iter()
.map(|p| match &p.inferred_type {
Some(ty) => format!("{} {}", p.name, ty),
None => format!("{} interface{{}}", p.name),
})
.collect::<Vec<_>>()
.join(", ");
let ret_str = match ret {
ReturnType::Unit => String::new(),
ReturnType::Single(v) => format!(" /* {} */", v),
ReturnType::Tuple(vs) => format!(" ({} /* multi-return */)", vs.join(", ")),
ReturnType::Result(ok, _) => format!(" ({}, error)", ok),
};
let return_stmt = match ret {
ReturnType::Unit => String::new(),
ReturnType::Single(v) => format!("\n{} return {}", indent, v),
ReturnType::Tuple(vs) => format!("\n{} return {}", indent, vs.join(", ")),
ReturnType::Result(ok, _) => format!("\n{} return {}, nil", indent, ok),
};
let body = body_lines
.iter()
.map(|l| format!("{} {}", indent, l))
.collect::<Vec<_>>()
.join("\n");
format!(
"\n{}func {}({}){} {{\n{}{}\n{}}}\n",
indent, name, param_str, ret_str, body, return_stmt, indent
)
}
fn generate_ts_function(
grammar: &str,
name: &str,
params: &[Parameter],
ret: &ReturnType,
is_async: bool,
body_lines: &[String],
indent: &str,
) -> String {
let async_kw = if is_async { "async " } else { "" };
let param_str = params
.iter()
.map(|p| match &p.inferred_type {
Some(ty) if grammar == "typescript" || grammar == "tsx" => {
format!("{}: {}", p.name, ty)
}
_ => p.name.clone(),
})
.collect::<Vec<_>>()
.join(", ");
let ret_annotation = if grammar == "typescript" || grammar == "tsx" {
match ret {
ReturnType::Unit => ": void".to_string(),
ReturnType::Single(v) => format!(": /* {} */", v),
ReturnType::Tuple(vs) => format!(
": [{}]",
vs.iter()
.map(|v| format!("/* {} */", v))
.collect::<Vec<_>>()
.join(", ")
),
ReturnType::Result(ok, _) => format!(": {} | Error", ok),
}
} else {
String::new()
};
let return_stmt = match ret {
ReturnType::Unit => String::new(),
ReturnType::Single(v) => format!("\n{} return {};", indent, v),
ReturnType::Tuple(vs) => format!("\n{} return [{}];", indent, vs.join(", ")),
ReturnType::Result(ok, _) => format!("\n{} return {};", indent, ok),
};
let body = body_lines
.iter()
.map(|l| format!("{} {}", indent, l))
.collect::<Vec<_>>()
.join("\n");
format!(
"\n{}{}function {}({}){} {{\n{}{}\n{}}}\n",
indent, async_kw, name, param_str, ret_annotation, body, return_stmt, indent
)
}
fn generate_java_function(
name: &str,
params: &[Parameter],
ret: &ReturnType,
body_lines: &[String],
indent: &str,
) -> String {
let ret_type = match ret {
ReturnType::Unit => "void".to_string(),
ReturnType::Single(v) => format!("/* {} */", v),
ReturnType::Tuple(vs) => format!("/* TODO: struct({}) */", vs.join(", ")),
ReturnType::Result(ok, err) => format!("/* {} throws {} */", ok, err),
};
let param_str = params
.iter()
.map(|p| match &p.inferred_type {
Some(ty) => format!("{} {}", ty, p.name),
None => format!("/* type */ {}", p.name),
})
.collect::<Vec<_>>()
.join(", ");
let return_stmt = match ret {
ReturnType::Unit => String::new(),
ReturnType::Single(v) => format!("\n{} return {};", indent, v),
ReturnType::Tuple(vs) => {
format!("\n{} // TODO: return struct({});", indent, vs.join(", "))
}
ReturnType::Result(ok, _) => format!("\n{} return {};", indent, ok),
};
let body = body_lines
.iter()
.map(|l| format!("{} {}", indent, l))
.collect::<Vec<_>>()
.join("\n");
format!(
"\n{}private {} {}({}) {{\n{}{}\n{}}}\n",
indent, ret_type, name, param_str, body, return_stmt, indent
)
}
fn generate_call_site(
grammar: &str,
name: &str,
params: &[Parameter],
ret: &ReturnType,
is_async: bool,
indent: &str,
) -> String {
let args = params
.iter()
.map(|p| p.name.as_str())
.collect::<Vec<_>>()
.join(", ");
let await_kw = if is_async {
match grammar {
"rust" => ".await",
_ => "await ",
}
} else {
""
};
match grammar {
"python" => match ret {
ReturnType::Unit => format!(
"{}{}{}{}",
indent,
await_kw,
name,
format_args!("({})", args)
),
ReturnType::Single(v) => {
format!("{}{} = {}{}({})\n", indent, v, await_kw, name, args)
}
ReturnType::Tuple(vs) => {
format!(
"{}{} = {}{}({})\n",
indent,
vs.join(", "),
await_kw,
name,
args
)
}
ReturnType::Result(ok, _) => format!("{}{} = {}({})\n", indent, ok, name, args),
},
"go" => match ret {
ReturnType::Unit => format!("{}{}({})\n", indent, name, args),
ReturnType::Single(v) => format!("{}{} := {}({})\n", indent, v, name, args),
ReturnType::Tuple(vs) => {
format!("{}{} := {}({})\n", indent, vs.join(", "), name, args)
}
ReturnType::Result(ok, _) => {
format!(
"{}{}, err := {}({})\n{}if err != nil {{ return err }}\n",
indent, ok, name, args, indent
)
}
},
"typescript" | "javascript" | "tsx" | "jsx" => match ret {
ReturnType::Unit => format!(
"{}{}{}{}",
indent,
await_kw,
name,
format_args!("({});", args)
),
ReturnType::Single(v) => {
let prefix = if await_kw == "await " { "await " } else { "" };
format!("{}const {} = {}{}({});\n", indent, v, prefix, name, args)
}
ReturnType::Tuple(vs) => {
let prefix = if await_kw == "await " { "await " } else { "" };
format!(
"{}const [{}] = {}{}({});\n",
indent,
vs.join(", "),
prefix,
name,
args
)
}
ReturnType::Result(ok, _) => {
let prefix = if await_kw == "await " { "await " } else { "" };
format!("{}const {} = {}{}({});\n", indent, ok, prefix, name, args)
}
},
"java" => match ret {
ReturnType::Unit => format!("{}{}({});\n", indent, name, args),
ReturnType::Single(v) => format!("{}var {} = {}({});\n", indent, v, name, args),
ReturnType::Tuple(vs) => {
format!(
"{}var result = {}({}); // TODO: unpack ({})\n",
indent,
name,
args,
vs.join(", ")
)
}
ReturnType::Result(ok, _) => format!("{}var {} = {}({});\n", indent, ok, name, args),
},
_ => {
match ret {
ReturnType::Unit => {
if is_async {
format!("{}{}({}).await;\n", indent, name, args)
} else {
format!("{}{}({});\n", indent, name, args)
}
}
ReturnType::Single(v) => {
if is_async {
format!("{}let {} = {}({}).await;\n", indent, v, name, args)
} else {
format!("{}let {} = {}({});\n", indent, v, name, args)
}
}
ReturnType::Tuple(vs) => {
if is_async {
format!(
"{}let ({}) = {}({}).await;\n",
indent,
vs.join(", "),
name,
args
)
} else {
format!("{}let ({}) = {}({});\n", indent, vs.join(", "), name, args)
}
}
ReturnType::Result(ok, _) => {
if is_async {
format!("{}let {} = {}({}).await?;\n", indent, ok, name, args)
} else {
format!("{}let {} = {}({})?;\n", indent, ok, name, args)
}
}
}
}
}
}
fn strip_common_indent(lines: &[&str]) -> Vec<String> {
let min_indent = lines
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
lines
.iter()
.map(|l| {
if l.len() >= min_indent {
l[min_indent..].to_string()
} else {
l.to_string()
}
})
.collect()
}
fn splice_content(
content: &str,
start_line: u32,
end_line: u32,
call_site: &str,
new_function: &str,
) -> Result<String, String> {
let lines: Vec<&str> = content.lines().collect();
let n = lines.len();
let start_idx = (start_line.saturating_sub(1)) as usize;
let end_idx = (end_line as usize).min(n);
if start_idx >= n {
return Err(format!(
"start line {} is beyond end of file ({} lines)",
start_line, n
));
}
let mut new_lines: Vec<&str> = Vec::new();
new_lines.extend_from_slice(&lines[..start_idx]);
let call_site_trimmed = call_site.trim_end_matches('\n');
for l in call_site_trimmed.lines() {
new_lines.push(l);
}
new_lines.extend_from_slice(&lines[end_idx..]);
let had_trailing = content.ends_with('\n');
let mut result = new_lines.join("\n");
if had_trailing {
result.push('\n');
}
result.push_str(new_function);
Ok(result)
}
pub fn parse_line_range(s: &str) -> Result<(u32, u32), String> {
match s.split_once('-') {
Some((a, b)) => {
let start = a
.trim()
.parse::<u32>()
.map_err(|_| format!("invalid start line in range '{}': expected integer", s))?;
let end = b
.trim()
.parse::<u32>()
.map_err(|_| format!("invalid end line in range '{}': expected integer", s))?;
if start == 0 || end == 0 {
return Err(format!(
"line range '{}': lines are 1-based (must be ≥ 1)",
s
));
}
if start > end {
return Err(format!(
"line range '{}': start ({}) must be ≤ end ({})",
s, start, end
));
}
Ok((start, end))
}
None => Err(format!(
"invalid line range '{}': expected 'start-end' (e.g. '10-25')",
s
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_line_range_basic() {
assert_eq!(parse_line_range("10-25").unwrap(), (10, 25));
}
#[test]
fn parse_line_range_single_line() {
assert_eq!(parse_line_range("5-5").unwrap(), (5, 5));
}
#[test]
fn parse_line_range_rejects_zero() {
assert!(parse_line_range("0-5").is_err());
}
#[test]
fn parse_line_range_rejects_inverted() {
assert!(parse_line_range("10-5").is_err());
}
#[test]
fn parse_line_range_rejects_missing_dash() {
assert!(parse_line_range("10").is_err());
}
#[test]
fn strip_common_indent_basic() {
let lines = vec![" let x = 1;", " let y = 2;"];
let out = strip_common_indent(&lines);
assert_eq!(out, vec!["let x = 1;", "let y = 2;"]);
}
#[test]
fn strip_common_indent_mixed() {
let lines = vec![" let x = 1;", " let y = 2;"];
let out = strip_common_indent(&lines);
assert_eq!(out, vec!["let x = 1;", " let y = 2;"]);
}
#[test]
fn generate_rust_fn_unit_return() {
let params = vec![Parameter {
name: "x".to_string(),
inferred_type: Some("i32".to_string()),
mutable: false,
}];
let body = vec!["println!(\"{}\", x);".to_string()];
let out = generate_rust_function(
"do_thing",
¶ms,
&ReturnType::Unit,
false,
false,
&body,
"",
);
assert!(out.contains("fn do_thing(x: i32)"));
assert!(out.contains("println!"));
assert!(!out.contains("->"));
}
#[test]
fn generate_rust_fn_single_return() {
let params = vec![];
let body = vec!["let result = 42;".to_string()];
let out = generate_rust_function(
"compute",
¶ms,
&ReturnType::Single("result".to_string()),
false,
false,
&body,
"",
);
assert!(out.contains("fn compute()"));
assert!(out.contains("-> /* result */"));
assert!(out.contains("result"));
}
#[test]
fn generate_rust_fn_async() {
let params = vec![];
let body = vec!["tokio::time::sleep(Duration::from_secs(1)).await;".to_string()];
let out = generate_rust_function(
"wait_a_bit",
¶ms,
&ReturnType::Unit,
true,
false,
&body,
"",
);
assert!(out.contains("async fn wait_a_bit()"));
}
#[test]
fn generate_python_fn_basic() {
let params = vec![Parameter {
name: "x".to_string(),
inferred_type: None,
mutable: false,
}];
let body = vec!["print(x)".to_string()];
let out = generate_python_function("show", ¶ms, &ReturnType::Unit, false, &body, "");
assert!(out.contains("def show(x):"));
assert!(out.contains("print(x)"));
}
#[test]
fn generate_python_fn_multi_return() {
let params = vec![];
let body = vec!["a = 1".to_string(), "b = 2".to_string()];
let out = generate_python_function(
"two_values",
¶ms,
&ReturnType::Tuple(vec!["a".to_string(), "b".to_string()]),
false,
&body,
"",
);
assert!(out.contains("return a, b"));
}
#[test]
fn generate_go_fn_basic() {
let params = vec![Parameter {
name: "n".to_string(),
inferred_type: Some("int".to_string()),
mutable: false,
}];
let body = vec!["result := n * 2".to_string()];
let out = generate_go_function(
"double",
¶ms,
&ReturnType::Single("result".to_string()),
&body,
"",
);
assert!(out.contains("func double(n int)"));
assert!(out.contains("return result"));
}
#[test]
fn splice_content_replaces_region() {
let content = "line1\nline2\nline3\nline4\nline5\n";
let result =
splice_content(content, 2, 3, " call()\n", "\nfn extracted() {}\n").unwrap();
assert!(result.contains("line1\n call()\nline4\nline5\n"));
assert!(result.contains("fn extracted()"));
}
#[test]
fn splice_content_preserves_surrounding_lines() {
let content = "a\nb\nc\nd\n";
let result = splice_content(content, 2, 2, "X\n", "\nfn f() {}\n").unwrap();
assert!(result.starts_with("a\nX\nc\nd\n"));
}
#[test]
fn rust_call_site_with_return() {
let params = vec![Parameter {
name: "x".to_string(),
inferred_type: Some("i32".to_string()),
mutable: false,
}];
let call = generate_call_site(
"rust",
"compute",
¶ms,
&ReturnType::Single("result".to_string()),
false,
" ",
);
assert_eq!(call, " let result = compute(x);\n");
}
#[test]
fn rust_call_site_async() {
let params = vec![];
let call = generate_call_site(
"rust",
"fetch",
¶ms,
&ReturnType::Single("data".to_string()),
true,
" ",
);
assert_eq!(call, " let data = fetch().await;\n");
}
#[test]
fn python_call_site_multi_return() {
let params = vec![];
let call = generate_call_site(
"python",
"two_vals",
¶ms,
&ReturnType::Tuple(vec!["a".to_string(), "b".to_string()]),
false,
"",
);
assert_eq!(call, "a, b = two_vals()\n");
}
}