use super::symbol_extract::innermost_qualified_name;
use super::{Language, child_text_by_kind, child_text_by_kinds, parse_source};
#[derive(Debug, Clone)]
pub struct FunctionSpan {
pub full_range: std::ops::Range<usize>,
pub signature_range: std::ops::Range<usize>,
pub signature_text: String,
pub name: String,
pub start_line: usize,
pub signature_end_line: usize,
}
pub fn splice_function_signature(
source: &str,
sig_range: std::ops::Range<usize>,
new_sig: &str,
) -> String {
let new_sig = new_sig.trim_end();
let start = sig_range.start.min(source.len());
let end = sig_range.end.min(source.len()).max(start);
let old_sig = &source[start..end];
let trimmed = old_sig.trim_end();
let logical_end = end - (old_sig.len() - trimmed.len());
let before = &source[..start];
let after = &source[logical_end..];
if after.starts_with('{')
&& !new_sig.is_empty()
&& !new_sig
.chars()
.last()
.map(|c| c.is_whitespace())
.unwrap_or(false)
{
format!("{before}{new_sig} {after}")
} else {
format!("{before}{new_sig}{after}")
}
}
pub fn find_function_span(
source: &str,
function_name: &str,
lang: Language,
) -> Option<FunctionSpan> {
let (tree, _) = parse_source(source, lang)?;
let root = tree.root_node();
let fn_node = find_function_node(root, source, function_name)?;
let start = fn_node.start_byte();
let end = fn_node.end_byte();
let sig_end = find_body_start(fn_node).unwrap_or(end);
let signature_text = source[start..sig_end].trim_end().to_string();
let start_line = fn_node.start_position().row + 1;
let sig_end_line = source[..sig_end].matches('\n').count() + 1;
Some(FunctionSpan {
full_range: start..end,
signature_range: start..sig_end,
signature_text,
name: function_name.to_string(),
start_line,
signature_end_line: sig_end_line,
})
}
const FUNCTION_NODE_KINDS: &[&str] = &[
"function_item", "function_definition", "function_declaration", "method_declaration", "method_definition", "constructor_declaration", ];
const BODY_NODE_KINDS: &[&str] = &[
"block", "statement_block", "compound_statement", ];
pub(crate) fn find_function_node<'a>(
node: tree_sitter_lib::Node<'a>,
source: &str,
name: &str,
) -> Option<tree_sitter_lib::Node<'a>> {
if FUNCTION_NODE_KINDS.contains(&node.kind()) && function_node_has_name(node, source, name) {
return Some(node);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(found) = find_function_node(child, source, name) {
return Some(found);
}
}
None
}
fn function_node_has_name(node: tree_sitter_lib::Node, source: &str, name: &str) -> bool {
let name_kinds = &[
"identifier",
"name",
"property_identifier",
"field_identifier",
"word",
];
if child_text_by_kinds(node, name_kinds, source) == Some(name) {
return true;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind().contains("declarator") {
if check_declarator_for_name(child, name_kinds, source, name) {
return true;
}
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if grandchild.kind().contains("declarator") || grandchild.kind() == "identifier" {
if grandchild.kind() == "identifier" {
if grandchild.utf8_text(source.as_bytes()).ok() == Some(name) {
return true;
}
} else if check_declarator_for_name(grandchild, name_kinds, source, name) {
return true;
}
}
}
}
}
false
}
fn check_declarator_for_name(
node: tree_sitter_lib::Node,
name_kinds: &[&str],
source: &str,
name: &str,
) -> bool {
if child_text_by_kinds(node, name_kinds, source) == Some(name) {
return true;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if (child.kind() == "qualified_identifier" || child.kind() == "scoped_identifier")
&& qualified_identifier_has_name(child, name_kinds, source, name)
{
return true;
}
}
false
}
fn qualified_identifier_has_name(
node: tree_sitter_lib::Node,
name_kinds: &[&str],
source: &str,
name: &str,
) -> bool {
innermost_qualified_name(node, name_kinds, source) == Some(name)
}
fn find_body_start(fn_node: tree_sitter_lib::Node) -> Option<usize> {
let mut cursor = fn_node.walk();
for child in fn_node.children(&mut cursor) {
if BODY_NODE_KINDS.contains(&child.kind()) {
return Some(child.start_byte());
}
}
None
}
pub fn replace_function_signature(source: &str, old_name: &str, new_sig: &str) -> Option<String> {
let (tree, _) = parse_source(source, Language::Rust)?;
let root = tree.root_node();
fn find_fn<'a>(
node: tree_sitter_lib::Node<'a>,
source: &str,
old_name: &str,
) -> Option<tree_sitter_lib::Node<'a>> {
if node.kind() == "function_item"
&& let Some(id) = child_text_by_kind(node, "identifier", source)
&& id == old_name
{
return Some(node);
}
let mut c = node.walk();
for child in node.children(&mut c) {
if let Some(found) = find_fn(child, source, old_name) {
return Some(found);
}
}
None
}
let fn_node = find_fn(root, source, old_name)?;
let sig_end = find_body_start(fn_node).unwrap_or(fn_node.end_byte());
let start = fn_node.start_byte();
Some(splice_function_signature(source, start..sig_end, new_sig))
}
#[derive(Debug, Clone, Default)]
pub struct FunctionSigEdit {
pub visibility: Option<String>,
pub parameters: Option<String>,
pub return_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseSigError {
pub message: String,
}
impl std::fmt::Display for ParseSigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ParseSigError {}
impl FunctionSigEdit {
pub fn parse_rust(sig: &str) -> Result<Self, ParseSigError> {
let s = sig.trim();
if s.is_empty() {
return Err(ParseSigError {
message: "empty signature".into(),
});
}
if s.starts_with('(') {
return parse_params_and_return(s);
}
let mut rest = s;
let mut visibility = None;
if rest.starts_with("pub(") {
if let Some(end) = rest.find(')') {
visibility = Some(rest[..=end].to_string());
rest = rest[end + 1..].trim_start();
}
} else if rest.starts_with("pub")
&& rest
.chars()
.nth(3)
.map(|c| c.is_whitespace())
.unwrap_or(false)
{
visibility = Some("pub".into());
rest = rest[3..].trim_start();
}
for q in ["async ", "const ", "unsafe ", "extern "] {
if rest.starts_with(q) {
rest = rest[q.len()..].trim_start();
if q.starts_with("extern")
&& rest.starts_with('"')
&& let Some(end) = rest[1..].find('"')
{
rest = rest[end + 2..].trim_start();
}
}
}
if rest.starts_with("fn ") {
rest = rest[3..].trim_start();
if let Some(paren) = rest.find('(') {
rest = &rest[paren..];
} else {
return Err(ParseSigError {
message: format!("expected parameters after function name in `{sig}`"),
});
}
} else if rest.starts_with("fn(") {
rest = &rest[2..]; }
if !rest.starts_with('(') {
return Err(ParseSigError {
message: format!("could not find parameter list in `{sig}`"),
});
}
let mut parsed = parse_params_and_return(rest)?;
if visibility.is_some() {
parsed.visibility = visibility;
}
Ok(parsed)
}
}
fn parse_params_and_return(s: &str) -> Result<FunctionSigEdit, ParseSigError> {
let mut depth = 0i32;
let mut params_end = None;
for (i, ch) in s.char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
params_end = Some(i + 1);
break;
}
}
_ => {}
}
}
let end = params_end.ok_or_else(|| ParseSigError {
message: format!("unbalanced parentheses in `{s}`"),
})?;
let parameters = Some(s[..end].to_string());
let after = s[end..].trim_start();
let return_type = if after.starts_with("->") {
Some(after.to_string())
} else if after.is_empty() {
None
} else {
let cleaned = after.trim_end_matches('{').trim();
if cleaned.starts_with("->") {
Some(cleaned.to_string())
} else {
None
}
};
Ok(FunctionSigEdit {
visibility: None,
parameters,
return_type,
})
}
pub fn rewrite_function_signature(
source: &str,
old_name: &str,
edit: &FunctionSigEdit,
lang: Language,
) -> Option<String> {
if lang == Language::Rust {
return rewrite_rust_sig(source, old_name, edit);
}
rewrite_sig_generic(source, old_name, edit, lang)
}
fn rewrite_rust_sig(source: &str, old_name: &str, edit: &FunctionSigEdit) -> Option<String> {
let (tree, _) = parse_source(source, Language::Rust)?;
let root = tree.root_node();
let fn_node = find_fn_for_rewrite(root, source, old_name)?;
let vis = edit.visibility.as_deref().unwrap_or_else(|| {
child_text_by_kind(fn_node, "visibility_modifier", source).unwrap_or("")
});
let params = edit
.parameters
.as_deref()
.unwrap_or_else(|| child_text_by_kind(fn_node, "parameters", source).unwrap_or("()"));
let ret = edit
.return_type
.as_deref()
.unwrap_or_else(|| extract_return_type(fn_node, source).unwrap_or(""));
let qualifiers = extract_fn_qualifiers(fn_node, source);
let vis_part = if vis.is_empty() {
String::new()
} else {
format!("{} ", vis)
};
let qual_part = if qualifiers.is_empty() {
String::new()
} else {
format!("{} ", qualifiers)
};
let ret_part = if ret.is_empty() {
String::new()
} else {
format!(" {}", ret)
};
let new_sig = format!(
"{}{}fn {}{}{}",
vis_part, qual_part, old_name, params, ret_part
);
let sig_end = find_body_start(fn_node).unwrap_or(fn_node.end_byte());
let start = fn_node.start_byte();
Some(splice_function_signature(source, start..sig_end, &new_sig))
}
const PARAM_NODE_KINDS: &[&str] = &[
"parameters", "formal_parameters", "parameter_list", ];
fn rewrite_sig_generic(
source: &str,
old_name: &str,
edit: &FunctionSigEdit,
lang: Language,
) -> Option<String> {
let (tree, _) = parse_source(source, lang)?;
let root = tree.root_node();
let fn_node = find_function_node(root, source, old_name)?;
let sig_end = find_body_start(fn_node).unwrap_or(fn_node.end_byte());
let sig_start = fn_node.start_byte();
let mut edits: Vec<(std::ops::Range<usize>, String)> = Vec::new();
if let Some(new_vis) = &edit.visibility {
if let Some(node) = find_modifier_node(fn_node, lang) {
let end = node.end_byte();
let ws_end = if source.as_bytes().get(end) == Some(&b' ') {
end + 1
} else {
end
};
if new_vis.is_empty() {
edits.push((node.start_byte()..ws_end, String::new()));
} else {
edits.push((node.start_byte()..end, new_vis.clone()));
}
} else if !new_vis.is_empty() {
edits.push((sig_start..sig_start, format!("{} ", new_vis)));
}
}
if let Some(new_params) = &edit.parameters {
let params_node = if fn_node.kind() == "method_declaration" {
fn_node
.child_by_field_name("parameters")
.or_else(|| find_nth_child_of_kinds(fn_node, PARAM_NODE_KINDS, 1))
} else {
find_first_child_of_kinds(fn_node, PARAM_NODE_KINDS)
};
if let Some(node) = params_node {
edits.push((node.start_byte()..node.end_byte(), new_params.clone()));
}
}
if let Some(new_ret) = &edit.return_type {
if let Some(range) = find_return_type_range(fn_node, lang, sig_end) {
if new_ret.is_empty() {
let trimmed_start = source[..range.start].trim_end().len();
edits.push((trimmed_start..range.end, String::new()));
} else {
edits.push((range, new_ret.clone()));
}
} else if !new_ret.is_empty() {
let insert_pos = if fn_node.kind() == "method_declaration" {
fn_node
.child_by_field_name("parameters")
.or_else(|| find_nth_child_of_kinds(fn_node, PARAM_NODE_KINDS, 1))
} else {
find_first_child_of_kinds(fn_node, PARAM_NODE_KINDS)
}
.map(|n| n.end_byte())
.unwrap_or(sig_end);
edits.push((insert_pos..insert_pos, format!(" {}", new_ret)));
}
}
edits.sort_by_key(|e| std::cmp::Reverse(e.0.start));
let mut result = source.to_string();
for (range, text) in edits {
result.replace_range(range, &text);
}
Some(result)
}
fn find_first_child_of_kinds<'a>(
node: tree_sitter_lib::Node<'a>,
kinds: &[&str],
) -> Option<tree_sitter_lib::Node<'a>> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if kinds.contains(&child.kind()) {
return Some(child);
}
if child.kind().contains("declarator") {
let mut inner = child.walk();
for grandchild in child.children(&mut inner) {
if kinds.contains(&grandchild.kind()) {
return Some(grandchild);
}
}
}
}
None
}
fn find_nth_child_of_kinds<'a>(
node: tree_sitter_lib::Node<'a>,
kinds: &[&str],
n: usize,
) -> Option<tree_sitter_lib::Node<'a>> {
let mut cursor = node.walk();
let mut count = 0;
for child in node.children(&mut cursor) {
if kinds.contains(&child.kind()) {
if count == n {
return Some(child);
}
count += 1;
}
}
None
}
fn find_modifier_node(
fn_node: tree_sitter_lib::Node,
lang: Language,
) -> Option<tree_sitter_lib::Node> {
match lang {
Language::Java => find_first_child_of_kinds(fn_node, &["modifiers"]),
_ => find_first_child_of_kinds(fn_node, &["visibility_modifier", "visibility"]),
}
}
fn find_return_type_range(
fn_node: tree_sitter_lib::Node,
lang: Language,
sig_end: usize,
) -> Option<std::ops::Range<usize>> {
match lang {
Language::Python => {
let mut cursor = fn_node.walk();
let mut arrow_start = None;
for child in fn_node.children(&mut cursor) {
if child.kind() == "->" {
arrow_start = Some(child.start_byte());
continue;
}
if let Some(a) = arrow_start
&& child.kind() == "type"
{
return Some(a..child.end_byte());
}
}
None
}
Language::Go => {
fn_node
.child_by_field_name("result")
.map(|n| n.start_byte()..n.end_byte())
}
Language::TypeScript | Language::JavaScript => {
find_first_child_of_kinds(fn_node, &["return_type", "type_annotation"])
.map(|n| n.start_byte()..n.end_byte())
}
Language::Java => {
let name_start = fn_node
.child_by_field_name("name")
.map(|n| n.start_byte())
.unwrap_or(sig_end);
let type_kinds = [
"void_type",
"type_identifier",
"generic_type",
"integral_type",
"boolean_type",
"floating_point_type",
"array_type",
"scoped_type_identifier",
];
let mut cursor = fn_node.walk();
for child in fn_node.children(&mut cursor) {
if child.start_byte() < name_start && type_kinds.contains(&child.kind()) {
return Some(child.start_byte()..child.end_byte());
}
}
None
}
Language::C | Language::Cpp => {
let decl_start =
find_first_child_of_kinds(fn_node, &["declarator", "function_declarator"])
.map(|n| n.start_byte())
.unwrap_or(sig_end);
let type_kinds = [
"primitive_type",
"type_identifier",
"sized_type_specifier",
"struct_specifier",
"enum_specifier",
"union_specifier",
"template_type",
];
let mut cursor = fn_node.walk();
for child in fn_node.children(&mut cursor) {
if child.start_byte() < decl_start && type_kinds.contains(&child.kind()) {
return Some(child.start_byte()..child.end_byte());
}
}
None
}
_ => None,
}
}
fn extract_return_type<'a>(fn_node: tree_sitter_lib::Node, source: &'a str) -> Option<&'a str> {
let mut cursor = fn_node.walk();
let mut arrow_start = None;
for child in fn_node.children(&mut cursor) {
if child.kind() == "->" {
arrow_start = Some(child.start_byte());
continue;
}
if let Some(a) = arrow_start {
return Some(source[a..child.end_byte()].trim_end());
}
}
None
}
fn extract_fn_qualifiers(fn_node: tree_sitter_lib::Node, source: &str) -> String {
let node_text = &source[fn_node.start_byte()..fn_node.end_byte()];
let fn_pos = match node_text.find("fn ") {
Some(pos) => pos,
None => return String::new(),
};
let prefix = &node_text[..fn_pos];
let vis = child_text_by_kind(fn_node, "visibility_modifier", source)
.or_else(|| child_text_by_kind(fn_node, "visibility", source));
let quals = if let Some(v) = vis {
let after_vis = prefix.strip_prefix(v).unwrap_or(prefix);
after_vis.trim()
} else {
prefix.trim()
};
quals.to_string()
}
fn find_fn_for_rewrite<'a>(
node: tree_sitter_lib::Node<'a>,
source: &str,
old_name: &str,
) -> Option<tree_sitter_lib::Node<'a>> {
if node.kind() == "function_item"
&& let Some(id) = child_text_by_kind(node, "identifier", source)
&& id == old_name
{
return Some(node);
}
let mut c = node.walk();
for child in node.children(&mut c) {
if let Some(found) = find_fn_for_rewrite(child, source, old_name) {
return Some(found);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rewrite_function_signature_structured() {
let src = "fn old(a: i32) -> i32 { a }\nfn other() {}";
let edit = FunctionSigEdit {
visibility: Some("pub(crate)".to_string()),
parameters: Some("(x: u32, y: &str)".to_string()),
return_type: Some("-> String".to_string()),
};
let res = rewrite_function_signature(src, "old", &edit, Language::Rust);
let out = res.expect("rewrite_function_signature should succeed for matching name");
assert!(out.contains("pub(crate) fn old(x: u32, y: &str) -> String"));
assert!(out.contains("fn other"));
assert!(!out.contains("fn old(a: i32)"));
}
#[test]
fn rewrite_function_signature_preserves_async() {
let src = "pub async fn process(data: &[u8]) -> Result<()> { Ok(()) }";
let edit = FunctionSigEdit {
visibility: None,
parameters: Some("(input: &str)".to_string()),
return_type: None,
};
let res = rewrite_function_signature(src, "process", &edit, Language::Rust);
let out = res.expect("rewrite should succeed");
assert!(
out.contains("pub async fn process(input: &str) -> Result<()>"),
"async qualifier should be preserved: {out}"
);
}
#[test]
fn rewrite_function_signature_preserves_unsafe() {
let src = "pub unsafe fn dangerous(ptr: *const u8) {}";
let edit = FunctionSigEdit {
visibility: None,
parameters: Some("(ptr: *mut u8)".to_string()),
return_type: None,
};
let res = rewrite_function_signature(src, "dangerous", &edit, Language::Rust);
let out = res.expect("rewrite should succeed");
assert!(
out.contains("pub unsafe fn dangerous(ptr: *mut u8)"),
"unsafe qualifier should be preserved: {out}"
);
}
#[test]
fn rewrite_python_params() {
let src = "def process(data: list) -> dict:\n return {}\n";
let edit = FunctionSigEdit {
parameters: Some("(data: list, validate: bool = True)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Python).unwrap();
assert!(
out.contains("def process(data: list, validate: bool = True) -> dict:"),
"should replace params: {out}"
);
assert!(out.contains("return {}"), "body preserved");
}
#[test]
fn rewrite_python_return_type() {
let src = "def process(data: list) -> dict:\n return {}\n";
let edit = FunctionSigEdit {
return_type: Some("-> list".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Python).unwrap();
assert!(
out.contains("def process(data: list) -> list:"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_python_add_return_type() {
let src = "def process(data):\n pass\n";
let edit = FunctionSigEdit {
return_type: Some("-> dict".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Python).unwrap();
assert!(out.contains("-> dict"), "should add return type: {out}");
}
#[test]
fn rewrite_python_async_preserved() {
let src = "async def fetch(url: str) -> bytes:\n pass\n";
let edit = FunctionSigEdit {
parameters: Some("(url: str, timeout: int = 30)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "fetch", &edit, Language::Python).unwrap();
assert!(
out.contains("async def fetch(url: str, timeout: int = 30)"),
"async should be preserved: {out}"
);
}
#[test]
fn rewrite_typescript_params() {
let src = "function fetchData(url: string): Promise<Response> {\n return fetch(url);\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(url: string, options?: RequestInit)".to_string()),
..Default::default()
};
let out =
rewrite_function_signature(src, "fetchData", &edit, Language::TypeScript).unwrap();
assert!(
out.contains("function fetchData(url: string, options?: RequestInit)"),
"should replace params: {out}"
);
assert!(out.contains("return fetch(url)"), "body preserved");
}
#[test]
fn rewrite_typescript_return_type() {
let src = "function fetchData(url: string): Promise<Response> {\n return fetch(url);\n}\n";
let edit = FunctionSigEdit {
return_type: Some(": Promise<Result[]>".to_string()),
..Default::default()
};
let out =
rewrite_function_signature(src, "fetchData", &edit, Language::TypeScript).unwrap();
assert!(
out.contains(": Promise<Result[]>"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_go_function_params() {
let src =
"func HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let edit = FunctionSigEdit {
parameters: Some(
"(ctx context.Context, w http.ResponseWriter, r *http.Request)".to_string(),
),
..Default::default()
};
let out = rewrite_function_signature(src, "HandleRequest", &edit, Language::Go).unwrap();
assert!(
out.contains(
"func HandleRequest(ctx context.Context, w http.ResponseWriter, r *http.Request)"
),
"should replace params: {out}"
);
assert!(out.contains("return nil"), "body preserved");
}
#[test]
fn rewrite_go_function_return_type() {
let src =
"func HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let edit = FunctionSigEdit {
return_type: Some("(*Response, error)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "HandleRequest", &edit, Language::Go).unwrap();
assert!(
out.contains("(*Response, error)"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_go_method_params() {
let src = "func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(ctx context.Context, r *http.Request)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "HandleRequest", &edit, Language::Go).unwrap();
assert!(
out.contains("func (s *Server) HandleRequest(ctx context.Context, r *http.Request)"),
"should replace method params without touching receiver: {out}"
);
}
#[test]
fn rewrite_java_params() {
let src = "public class Foo {\n public void processEvent(Event e) {\n // body\n }\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(Event e, Config config)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "processEvent", &edit, Language::Java).unwrap();
assert!(
out.contains("processEvent(Event e, Config config)"),
"should replace params: {out}"
);
}
#[test]
fn rewrite_java_return_type() {
let src = "public class Foo {\n public void processEvent(Event e) {\n // body\n }\n}\n";
let edit = FunctionSigEdit {
return_type: Some("Response".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "processEvent", &edit, Language::Java).unwrap();
assert!(
out.contains("public Response processEvent"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_java_visibility() {
let src = "public class Foo {\n public void processEvent(Event e) {\n // body\n }\n}\n";
let edit = FunctionSigEdit {
visibility: Some("protected".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "processEvent", &edit, Language::Java).unwrap();
assert!(
out.contains("protected void processEvent"),
"should replace visibility: {out}"
);
}
#[test]
fn rewrite_c_params() {
let src = "int process(const char *input) {\n return 0;\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(const char *input, size_t len, int flags)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::C).unwrap();
assert!(
out.contains("int process(const char *input, size_t len, int flags)"),
"should replace params: {out}"
);
assert!(out.contains("return 0"), "body preserved");
}
#[test]
fn rewrite_c_return_type() {
let src = "int process(const char *input) {\n return 0;\n}\n";
let edit = FunctionSigEdit {
return_type: Some("void".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::C).unwrap();
assert!(
out.contains("void process(const char *input)"),
"should replace return type: {out}"
);
}
#[test]
fn rewrite_cpp_params() {
let src = "void MyClass::process(int x) {\n // body\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(int x, double y)".to_string()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Cpp).unwrap();
assert!(
out.contains("void MyClass::process(int x, double y)"),
"should replace C++ method params: {out}"
);
}
#[test]
fn rewrite_unsupported_language_returns_none() {
let src = "whatever";
let edit = FunctionSigEdit::default();
assert!(rewrite_function_signature(src, "foo", &edit, Language::Unknown).is_none());
}
#[test]
fn rewrite_function_not_found_returns_none() {
let src = "def process(data):\n pass\n";
let edit = FunctionSigEdit {
parameters: Some("(x)".to_string()),
..Default::default()
};
assert!(rewrite_function_signature(src, "nonexistent", &edit, Language::Python).is_none());
}
#[test]
fn parse_rust_pub_fn_with_return() {
let e = FunctionSigEdit::parse_rust("pub fn foo(x: i32) -> String").unwrap();
assert_eq!(e.visibility.as_deref(), Some("pub"));
assert_eq!(e.parameters.as_deref(), Some("(x: i32)"));
assert_eq!(e.return_type.as_deref(), Some("-> String"));
}
#[test]
fn parse_rust_pub_crate() {
let e = FunctionSigEdit::parse_rust("pub(crate) fn bar(a: u8)").unwrap();
assert_eq!(e.visibility.as_deref(), Some("pub(crate)"));
assert_eq!(e.parameters.as_deref(), Some("(a: u8)"));
assert!(e.return_type.is_none());
}
#[test]
fn parse_rust_params_only() {
let e = FunctionSigEdit::parse_rust("(x: i32) -> bool").unwrap();
assert!(e.visibility.is_none());
assert_eq!(e.parameters.as_deref(), Some("(x: i32)"));
assert_eq!(e.return_type.as_deref(), Some("-> bool"));
}
#[test]
fn parse_rust_nested_parens_in_params() {
let e = FunctionSigEdit::parse_rust("fn name(a: Fn(i32) -> bool, b: Vec<(u8, u8)>) -> R")
.unwrap();
assert_eq!(
e.parameters.as_deref(),
Some("(a: Fn(i32) -> bool, b: Vec<(u8, u8)>)")
);
assert_eq!(e.return_type.as_deref(), Some("-> R"));
}
#[test]
fn parse_rust_empty_is_err() {
let err = FunctionSigEdit::parse_rust("").expect_err("empty");
assert!(
err.message.contains("empty"),
"expected empty-signature error, got: {}",
err.message
);
let err2 = FunctionSigEdit::parse_rust(" ").expect_err("whitespace-only");
assert!(
err2.message.contains("empty"),
"expected empty-signature error, got: {}",
err2.message
);
}
#[test]
fn splice_function_signature_empty_new_sig_does_not_panic() {
let source = "fn f() -> i32 {\n 0\n}\n";
let span = find_function_span(source, "f", Language::Rust).unwrap();
let out = splice_function_signature(source, span.signature_range, " ");
assert!(out.contains("{"), "body brace remains: {out}");
assert!(!out.contains("i32{"), "got: {out}");
}
#[test]
fn find_function_span_rust() {
let source = "fn hello(x: i32) -> String {\n x.to_string()\n}\n";
let span = find_function_span(source, "hello", Language::Rust).unwrap();
assert_eq!(span.name, "hello");
assert!(span.signature_text.contains("fn hello(x: i32) -> String"));
assert!(!span.signature_text.contains("x.to_string()"));
assert_eq!(span.start_line, 1);
}
#[test]
fn find_function_span_python() {
let source = "class Foo:\n def process(self, data: list) -> dict:\n return {}\n";
let span = find_function_span(source, "process", Language::Python).unwrap();
assert_eq!(span.name, "process");
assert!(span.signature_text.contains("def process"));
assert!(span.signature_text.contains("-> dict"));
}
#[test]
fn find_function_span_typescript() {
let source = "export async function fetchData(url: string): Promise<Response> {\n return fetch(url);\n}\n";
let span = find_function_span(source, "fetchData", Language::TypeScript).unwrap();
assert_eq!(span.name, "fetchData");
assert!(span.signature_text.contains("function fetchData"));
}
#[test]
fn find_function_span_go_function() {
let source =
"func HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let span = find_function_span(source, "HandleRequest", Language::Go).unwrap();
assert_eq!(span.name, "HandleRequest");
assert!(span.signature_text.contains("func HandleRequest"));
}
#[test]
fn find_function_span_go_method() {
let source = "func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) error {\n\treturn nil\n}\n";
let span = find_function_span(source, "HandleRequest", Language::Go).unwrap();
assert_eq!(span.name, "HandleRequest");
assert!(
span.signature_text
.contains("func (s *Server) HandleRequest")
);
}
#[test]
fn find_function_span_java() {
let source = "public class Foo {\n public void processEvent(Event e) {\n // ...\n }\n}\n";
let span = find_function_span(source, "processEvent", Language::Java).unwrap();
assert_eq!(span.name, "processEvent");
assert!(span.signature_text.contains("public void processEvent"));
}
#[test]
fn find_function_span_not_found() {
let source = "fn hello() {}\n";
let result = find_function_span(source, "nonexistent", Language::Rust);
assert!(result.is_none());
}
#[test]
fn find_function_span_no_grammar() {
let source = "whatever";
let result = find_function_span(source, "foo", Language::Unknown);
assert!(result.is_none());
}
#[test]
fn find_function_span_multiline_python() {
let source = "def long_function(\n param1: str,\n param2: int,\n param3: bool = False,\n) -> dict:\n return {}\n";
let span = find_function_span(source, "long_function", Language::Python).unwrap();
assert_eq!(span.name, "long_function");
assert!(span.signature_text.contains("param1: str"));
assert!(span.signature_text.contains("-> dict"));
assert_eq!(span.start_line, 1);
assert!(span.signature_end_line >= 5);
}
#[test]
fn find_function_span_can_splice_replacement() {
let source = "fn old_name(x: i32) -> bool {\n true\n}\n";
let span = find_function_span(source, "old_name", Language::Rust).unwrap();
let result = splice_function_signature(
source,
span.signature_range,
"fn new_name(x: i32, y: bool) -> bool",
);
assert!(result.contains("fn new_name(x: i32, y: bool) -> bool {"));
assert!(!result.contains("bool{"));
assert!(result.contains("true")); }
#[test]
fn replace_function_signature_preserves_space_before_brace() {
let source = "pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n";
let out =
replace_function_signature(source, "add", "pub fn add(a: i32, b: i32, c: i32) -> i32")
.expect("replace");
assert!(
out.contains("-> i32 {"),
"must keep space before body brace, got: {out}"
);
assert!(
!out.contains("i32{"),
"must not glue return type to brace, got: {out}"
);
assert!(out.contains("a + b"), "body preserved: {out}");
}
#[test]
fn replace_function_signature_preserves_newline_before_brace() {
let source = "fn wrapped(x: i32) -> i32\n{\n x\n}\n";
let out =
replace_function_signature(source, "wrapped", "fn wrapped(x: i32, y: i32) -> i32")
.expect("replace");
assert!(
out.contains("-> i32\n{") || out.contains("-> i32 \n{"),
"must preserve newline before brace, got: {out}"
);
assert!(!out.contains("i32{"), "got: {out}");
}
#[test]
fn replace_function_signature_inserts_space_when_already_glued() {
let source = "fn glued(x: i32) -> i32{\n x\n}\n";
let out = replace_function_signature(source, "glued", "fn glued(x: i32) -> u64")
.expect("replace");
assert!(
out.contains("-> u64 {"),
"must insert conventional space when gap missing, got: {out}"
);
assert!(!out.contains("u64{"), "got: {out}");
}
#[test]
fn rewrite_function_signature_structured_keeps_space_before_brace() {
let src = "pub fn process(x: i32) -> i32 {\n x\n}\n";
let edit = FunctionSigEdit {
parameters: Some("(x: i32, y: i32)".into()),
return_type: Some("-> i64".into()),
..Default::default()
};
let out = rewrite_function_signature(src, "process", &edit, Language::Rust)
.expect("structured rewrite");
assert!(
out.contains("-> i64 {"),
"structured path must keep space before brace, got: {out}"
);
assert!(!out.contains("i64{"), "got: {out}");
}
#[test]
fn splice_function_signature_no_spurious_space_before_semicolon() {
let source = "fn foo();";
let out = splice_function_signature(source, 0..source.len() - 1, "fn foo(x: i32)");
assert_eq!(
out, "fn foo(x: i32);",
"must not insert space before semicolon"
);
assert!(!out.contains(") ;"));
}
#[test]
fn find_function_span_cpp() {
let source = "void process(int x, double y) {\n // body\n}\n";
let span = find_function_span(source, "process", Language::Cpp).unwrap();
assert_eq!(span.name, "process");
assert!(
span.signature_text
.contains("void process(int x, double y)")
);
assert!(!span.signature_text.contains("// body"));
}
#[test]
fn find_function_span_cpp_qualified_name() {
let source = "void MyClass::process(int x) {\n // body\n}\n";
let span = find_function_span(source, "process", Language::Cpp).unwrap();
assert_eq!(span.name, "process");
assert!(span.signature_text.contains("void MyClass::process(int x)"));
}
#[test]
fn find_function_span_cpp_nested_namespace_qualified() {
let source = "int ns::MyClass::deep() {\n return 0;\n}\n";
let span = find_function_span(source, "deep", Language::Cpp).unwrap();
assert_eq!(span.name, "deep");
assert!(span.signature_text.contains("ns::MyClass::deep()"));
}
#[test]
fn find_function_span_cpp_triple_nested_namespace() {
let source = "int outer::inner::MyClass::compute(int x) {\n return x;\n}\n";
let span = find_function_span(source, "compute", Language::Cpp).unwrap();
assert_eq!(span.name, "compute");
assert!(
span.signature_text
.contains("outer::inner::MyClass::compute(int x)")
);
}
#[test]
fn find_function_span_cpp_qualified_no_false_positive() {
let source = "void MyClass::alpha(int x) {\n}\nvoid MyClass::beta() {\n}\n";
let span = find_function_span(source, "beta", Language::Cpp).unwrap();
assert_eq!(span.name, "beta");
assert!(span.signature_text.contains("beta"));
assert!(!span.signature_text.contains("alpha"));
}
#[test]
fn find_function_span_c() {
let source = "int main(int argc, char *argv[]) {\n return 0;\n}\n";
let span = find_function_span(source, "main", Language::C).unwrap();
assert_eq!(span.name, "main");
assert!(span.signature_text.contains("int main"));
assert!(!span.signature_text.contains("return 0"));
}
}