use super::{find_node_at_byte, node_text, CodeEdit};
pub fn fix_python(
source: &str,
tree: &tree_sitter::Tree,
sink_start: usize,
sink_end: usize,
) -> Option<Vec<CodeEdit>> {
let root = tree.root_node();
let call_node = find_call_at(root, sink_start, sink_end)?;
let args = call_node.child_by_field_name("arguments")?;
let first_arg = first_positional_arg(args)?;
match first_arg.kind() {
"string"
if source[first_arg.start_byte()..first_arg.end_byte()].starts_with("f\"")
|| source[first_arg.start_byte()..first_arg.end_byte()].starts_with("f'") =>
{
fix_python_fstring(source, call_node, args, first_arg)
}
"binary_operator" | "concatenated_string" => {
fix_python_concat(source, call_node, args, first_arg)
}
_ => None,
}
}
pub fn fix_javascript(
source: &str,
tree: &tree_sitter::Tree,
sink_start: usize,
sink_end: usize,
) -> Option<Vec<CodeEdit>> {
let root = tree.root_node();
let call_node = find_call_at(root, sink_start, sink_end)?;
let args = call_node.child_by_field_name("arguments")?;
let first_arg = first_positional_arg(args)?;
match first_arg.kind() {
"template_string" => fix_js_template(source, call_node, args, first_arg),
"binary_expression" => fix_js_concat(source, call_node, args, first_arg),
_ => None,
}
}
pub fn fix_go(
source: &str,
tree: &tree_sitter::Tree,
sink_start: usize,
sink_end: usize,
) -> Option<Vec<CodeEdit>> {
let root = tree.root_node();
let call_node = find_call_at(root, sink_start, sink_end)?;
let args = call_node.child_by_field_name("arguments")?;
let first_arg = first_positional_arg(args)?;
if first_arg.kind() == "binary_expression" {
fix_go_concat(source, call_node, args, first_arg)
} else {
None
}
}
fn fix_python_fstring(
source: &str,
call_node: tree_sitter::Node,
_args_node: tree_sitter::Node,
fstr_node: tree_sitter::Node,
) -> Option<Vec<CodeEdit>> {
let fstr_text = node_text(fstr_node, source);
let mut params = Vec::new();
let mut cursor = fstr_node.walk();
for child in fstr_node.named_children(&mut cursor) {
if child.kind() == "interpolation" {
let mut inner_cursor = child.walk();
for inner in child.named_children(&mut inner_cursor) {
if inner.kind() != "type_conversion" && inner.kind() != "format_specifier" {
params.push(node_text(inner, source).to_string());
break;
}
}
}
}
if params.is_empty() {
return None;
}
let quote_char = if fstr_text.starts_with("f\"") {
'"'
} else {
'\''
};
let mut query = String::new();
query.push(quote_char);
let mut cursor2 = fstr_node.walk();
for child in fstr_node.children(&mut cursor2) {
match child.kind() {
"interpolation" => query.push('?'),
"string_content" => query.push_str(node_text(child, source)),
_ => {}
}
}
query.push(quote_char);
let params_str = if params.len() == 1 {
format!("({},)", params[0])
} else {
format!("({})", params.join(", "))
};
let func_text = call_func_text(call_node, source)?;
let replacement = format!("{}({}, {})", func_text, query, params_str);
Some(vec![CodeEdit {
start_byte: call_node.start_byte(),
end_byte: call_node.end_byte(),
replacement,
}])
}
fn fix_python_concat(
source: &str,
call_node: tree_sitter::Node,
_args_node: tree_sitter::Node,
concat_node: tree_sitter::Node,
) -> Option<Vec<CodeEdit>> {
let (string_parts, var_parts) = extract_concat_parts(concat_node, source)?;
if var_parts.is_empty() {
return None;
}
let mut query = String::new();
for (i, part) in string_parts.iter().enumerate() {
query.push_str(part);
if i < var_parts.len() {
query.push('?');
}
}
let params_str = if var_parts.len() == 1 {
format!("({},)", var_parts[0])
} else {
format!("({})", var_parts.join(", "))
};
let func_text = call_func_text(call_node, source)?;
let replacement = format!("{}(\"{}\", {})", func_text, query, params_str);
Some(vec![CodeEdit {
start_byte: call_node.start_byte(),
end_byte: call_node.end_byte(),
replacement,
}])
}
fn fix_js_template(
source: &str,
call_node: tree_sitter::Node,
_args_node: tree_sitter::Node,
template_node: tree_sitter::Node,
) -> Option<Vec<CodeEdit>> {
let mut params = Vec::new();
let mut cursor = template_node.walk();
for child in template_node.named_children(&mut cursor) {
if child.kind() == "template_substitution" {
let mut inner_cursor = child.walk();
let first = child.named_children(&mut inner_cursor).next();
if let Some(inner) = first {
params.push(node_text(inner, source).to_string());
}
}
}
if params.is_empty() {
return None;
}
let mut query = String::new();
query.push('"');
let mut param_idx = 1;
let mut cursor2 = template_node.walk();
for child in template_node.children(&mut cursor2) {
match child.kind() {
"template_substitution" => {
query.push_str(&format!("${}", param_idx));
param_idx += 1;
}
"string_fragment" => query.push_str(node_text(child, source)),
_ => {}
}
}
query.push('"');
let func_text = call_func_text(call_node, source)?;
let replacement = format!("{}({}, [{}])", func_text, query, params.join(", "));
Some(vec![CodeEdit {
start_byte: call_node.start_byte(),
end_byte: call_node.end_byte(),
replacement,
}])
}
fn fix_js_concat(
source: &str,
call_node: tree_sitter::Node,
_args_node: tree_sitter::Node,
concat_node: tree_sitter::Node,
) -> Option<Vec<CodeEdit>> {
let (string_parts, var_parts) = extract_concat_parts(concat_node, source)?;
if var_parts.is_empty() {
return None;
}
let mut query = String::new();
for (i, part) in string_parts.iter().enumerate() {
query.push_str(part);
if i < var_parts.len() {
query.push_str(&format!("${}", i + 1));
}
}
let func_text = call_func_text(call_node, source)?;
let replacement = format!("{}(\"{}\", [{}])", func_text, query, var_parts.join(", "));
Some(vec![CodeEdit {
start_byte: call_node.start_byte(),
end_byte: call_node.end_byte(),
replacement,
}])
}
fn fix_go_concat(
source: &str,
call_node: tree_sitter::Node,
_args_node: tree_sitter::Node,
concat_node: tree_sitter::Node,
) -> Option<Vec<CodeEdit>> {
let (string_parts, var_parts) = extract_concat_parts(concat_node, source)?;
if var_parts.is_empty() {
return None;
}
let mut query = String::new();
for (i, part) in string_parts.iter().enumerate() {
query.push_str(part);
if i < var_parts.len() {
query.push_str(&format!("${}", i + 1));
}
}
let func_text = call_func_text(call_node, source)?;
let replacement = format!("{}(\"{}\", {})", func_text, query, var_parts.join(", "));
Some(vec![CodeEdit {
start_byte: call_node.start_byte(),
end_byte: call_node.end_byte(),
replacement,
}])
}
fn find_call_at(root: tree_sitter::Node, start: usize, end: usize) -> Option<tree_sitter::Node> {
let node = find_node_at_byte(root, start)?;
let mut current = node;
loop {
if current.kind() == "call" || current.kind() == "call_expression" {
if current.start_byte() <= start && current.end_byte() >= end {
return Some(current);
}
}
current = current.parent()?;
}
}
fn call_func_text<'a>(call_node: tree_sitter::Node, source: &'a str) -> Option<&'a str> {
let func = call_node
.child_by_field_name("function")
.or_else(|| call_node.child_by_field_name("callee"))?;
Some(node_text(func, source))
}
fn first_positional_arg(args: tree_sitter::Node) -> Option<tree_sitter::Node> {
let mut cursor = args.walk();
let result = args
.named_children(&mut cursor)
.find(|child| child.kind() != "keyword_argument" && child.kind() != "spread_element");
result
}
fn extract_concat_parts(
node: tree_sitter::Node,
source: &str,
) -> Option<(Vec<String>, Vec<String>)> {
let mut leaves = Vec::new();
collect_concat_leaves(node, source, &mut leaves);
if leaves.is_empty() {
return None;
}
let mut string_parts = Vec::new();
let mut var_parts = Vec::new();
let mut current_string = String::new();
for leaf in &leaves {
match leaf {
ConcatLeaf::StringLit(s) => current_string.push_str(s),
ConcatLeaf::Variable(v) => {
string_parts.push(current_string.clone());
current_string.clear();
var_parts.push(v.clone());
}
}
}
string_parts.push(current_string);
if var_parts.is_empty() {
return None;
}
Some((string_parts, var_parts))
}
#[derive(Debug)]
enum ConcatLeaf {
StringLit(String),
Variable(String),
}
fn collect_concat_leaves(node: tree_sitter::Node, source: &str, out: &mut Vec<ConcatLeaf>) {
match node.kind() {
"binary_operator" | "binary_expression" => {
let op = node.child_by_field_name("operator");
let is_plus = match op {
Some(op_node) => node_text(op_node, source) == "+",
None => {
let text = node_text(node, source);
text.contains('+')
}
};
if !is_plus {
out.push(ConcatLeaf::Variable(node_text(node, source).to_string()));
return;
}
let left = node.child_by_field_name("left");
let right = node.child_by_field_name("right");
if let Some(l) = left {
collect_concat_leaves(l, source, out);
}
if let Some(r) = right {
collect_concat_leaves(r, source, out);
}
}
"string" | "interpreted_string_literal" | "raw_string_literal" => {
let text = node_text(node, source);
let inner = strip_string_quotes(text);
out.push(ConcatLeaf::StringLit(inner.to_string()));
}
_ => {
out.push(ConcatLeaf::Variable(node_text(node, source).to_string()));
}
}
}
fn strip_string_quotes(s: &str) -> &str {
if ((s.starts_with('"') && s.ends_with('"'))
|| (s.starts_with('\'') && s.ends_with('\''))
|| (s.starts_with('`') && s.ends_with('`')))
&& s.len() >= 2
{
&s[1..s.len() - 1]
} else {
s
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::parse_file;
use crate::Language;
#[test]
fn test_fix_python_fstring() {
let source = r#"cursor.execute(f"SELECT * FROM users WHERE name = {name}")"#;
let tree = parse_file(source, Language::Python).unwrap();
let edits = fix_python(source, &tree, 0, source.len()).unwrap();
assert_eq!(edits.len(), 1);
assert_eq!(
edits[0].replacement,
r#"cursor.execute("SELECT * FROM users WHERE name = ?", (name,))"#
);
}
#[test]
fn test_fix_python_concat() {
let source = r#"cursor.execute("SELECT * FROM users WHERE name = " + name)"#;
let tree = parse_file(source, Language::Python).unwrap();
let edits = fix_python(source, &tree, 0, source.len()).unwrap();
assert_eq!(edits.len(), 1);
assert_eq!(
edits[0].replacement,
r#"cursor.execute("SELECT * FROM users WHERE name = ?", (name,))"#
);
}
#[test]
fn test_fix_js_template() {
let source = "db.query(`SELECT * FROM users WHERE name = ${name}`)";
let tree = parse_file(source, Language::JavaScript).unwrap();
let edits = fix_javascript(source, &tree, 0, source.len()).unwrap();
assert_eq!(edits.len(), 1);
assert_eq!(
edits[0].replacement,
r#"db.query("SELECT * FROM users WHERE name = $1", [name])"#
);
}
#[test]
fn test_fix_js_concat() {
let source = r#"db.query("SELECT * FROM users WHERE name = " + name)"#;
let tree = parse_file(source, Language::JavaScript).unwrap();
let edits = fix_javascript(source, &tree, 0, source.len()).unwrap();
assert_eq!(edits.len(), 1);
assert_eq!(
edits[0].replacement,
r#"db.query("SELECT * FROM users WHERE name = $1", [name])"#
);
}
#[test]
fn test_fix_go_concat() {
let source = r#"db.Query("SELECT * FROM users WHERE name = " + name)"#;
let tree = parse_file(source, Language::Go).unwrap();
let edits = fix_go(source, &tree, 0, source.len());
if let Some(edits) = edits {
assert_eq!(edits.len(), 1);
assert!(edits[0].replacement.contains("$1"));
}
}
}