use super::conditions::unwrap_parens;
use super::{
anon_fn_name, collect_idents, collect_idents_with_paths, find_constructor_type_child,
first_call_ident, root_receiver_text, text_of,
};
use crate::labels::{Cap, Kind, lookup};
use tree_sitter::Node;
pub(super) fn find_call_node<'a>(n: Node<'a>, lang: &str) -> Option<Node<'a>> {
match lookup(lang, n.kind()) {
Kind::CallFn | Kind::CallMethod | Kind::CallMacro => Some(n),
_ => {
let mut cursor = n.walk();
for c in n.children(&mut cursor) {
match lookup(lang, c.kind()) {
Kind::CallFn | Kind::CallMethod | Kind::CallMacro => return Some(c),
_ => {}
}
}
let mut cursor2 = n.walk();
for c in n.children(&mut cursor2) {
let mut cursor3 = c.walk();
for gc in c.children(&mut cursor3) {
if matches!(
lookup(lang, gc.kind()),
Kind::CallFn | Kind::CallMethod | Kind::CallMacro
) {
return Some(gc);
}
}
}
None
}
}
}
pub(super) fn extract_destination_field_pairs(
call_node: Node,
arg_index: usize,
fields: &[&str],
code: &[u8],
) -> Option<Vec<(String, String)>> {
if fields.is_empty() {
return None;
}
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let arg = args.named_children(&mut cursor).nth(arg_index)?;
if !matches!(arg.kind(), "object" | "dictionary") {
return None;
}
let mut out: Vec<(String, String)> = Vec::new();
let mut c = arg.walk();
for child in arg.named_children(&mut c) {
match child.kind() {
"spread_element" | "dictionary_splat" => {
return None;
}
"shorthand_property_identifier" | "shorthand_property_identifier_pattern" => {
let Some(name) = text_of(child, code) else {
continue;
};
if fields.iter().any(|&f| f == name) && !out.iter().any(|(_, v)| v == &name) {
out.push((name.clone(), name));
}
}
"pair" => {
let Some(key_node) = child.child_by_field_name("key") else {
continue;
};
let key_text = match key_node.kind() {
"string" | "string_literal" => text_of(key_node, code).map(|raw| {
if raw.len() >= 2 {
raw[1..raw.len() - 1].to_string()
} else {
raw
}
}),
"computed_property_name" => continue,
_ => text_of(key_node, code),
};
let Some(key) = key_text else {
continue;
};
if !fields.iter().any(|&f| f == key) {
continue;
}
let Some(val_node) = child.child_by_field_name("value") else {
continue;
};
let mut idents: Vec<String> = Vec::new();
let mut paths: Vec<String> = Vec::new();
collect_idents_with_paths(val_node, code, &mut idents, &mut paths);
for name in paths.into_iter().chain(idents) {
if !out.iter().any(|(_, v)| v == &name) {
out.push((key.clone(), name));
}
}
}
_ => {}
}
}
Some(out)
}
pub(super) fn extract_destination_kwarg_pairs(
call_node: Node,
fields: &[&str],
code: &[u8],
) -> Vec<(String, String)> {
if fields.is_empty() {
return Vec::new();
}
let Some(args_node) = call_node.child_by_field_name("arguments") else {
return Vec::new();
};
let mut out: Vec<(String, String)> = Vec::new();
let mut cursor = args_node.walk();
for child in args_node.named_children(&mut cursor) {
let kind = child.kind();
if kind != "keyword_argument" && kind != "named_argument" {
continue;
}
let named_count = child.named_child_count();
let name_node = child
.child_by_field_name("name")
.or_else(|| child.named_child(0));
let value_node = child
.child_by_field_name("value")
.or_else(|| child.named_child(named_count.saturating_sub(1) as u32));
let (Some(nn), Some(vn)) = (name_node, value_node) else {
continue;
};
let Some(name) = text_of(nn, code) else {
continue;
};
if !fields.iter().any(|&f| f == name) {
continue;
}
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(vn, code, &mut idents, &mut paths);
for ident in paths.into_iter().chain(idents) {
if !out.iter().any(|(_, v)| v == &ident) {
out.push((name.clone(), ident));
}
}
}
out
}
pub(super) fn extract_const_string_arg(
call_node: Node,
index: usize,
code: &[u8],
) -> Option<String> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let mut arg = args.named_children(&mut cursor).nth(index)?;
if arg.kind() == "argument" && arg.named_child_count() == 1 {
if let Some(inner) = arg.named_child(0) {
arg = inner;
}
}
match arg.kind() {
"string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal" => {
let raw = text_of(arg, code)?;
if raw.len() >= 2 {
Some(raw[1..raw.len() - 1].to_string())
} else {
None
}
}
"template_string" => {
let mut c = arg.walk();
if arg
.named_children(&mut c)
.any(|ch| ch.kind() == "template_substitution")
{
return None; }
let raw = text_of(arg, code)?;
if raw.len() >= 2 {
Some(raw[1..raw.len() - 1].to_string())
} else {
None
}
}
_ => None,
}
}
pub(super) fn extract_const_macro_arg(
call_node: Node,
index: usize,
code: &[u8],
) -> Option<String> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let mut arg = args.named_children(&mut cursor).nth(index)?;
if arg.kind() == "argument" && arg.named_child_count() == 1 {
if let Some(inner) = arg.named_child(0) {
arg = inner;
}
}
match arg.kind() {
"identifier" | "name" | "qualified_name" | "scoped_identifier" => {
text_of(arg, code).map(|s| s.to_string())
}
_ => None,
}
}
pub(super) fn extract_const_keyword_arg(
call_node: Node,
keyword_name: &str,
code: &[u8],
) -> Option<String> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
for child in args.named_children(&mut cursor) {
if child.kind() == "keyword_argument" || child.kind() == "named_argument" {
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let Some(name_text) = text_of(name_node, code) else {
continue;
};
if name_text != keyword_name {
continue;
}
let value_node = child.child_by_field_name("value")?;
return match value_node.kind() {
"true" | "false" | "none" | "integer" | "float" | "string" | "string_literal"
| "identifier" => text_of(value_node, code).map(|s| s.to_string()),
_ => None,
}
.filter(|_| {
match value_node.kind() {
"identifier" => text_of(value_node, code)
.as_deref()
.is_some_and(|s| matches!(s, "True" | "False" | "None")),
_ => true,
}
});
}
}
None
}
pub(super) fn has_keyword_arg(call_node: Node, keyword_name: &str, code: &[u8]) -> bool {
let Some(args) = call_node.child_by_field_name("arguments") else {
return false;
};
let mut cursor = args.walk();
for child in args.named_children(&mut cursor) {
if child.kind() != "keyword_argument" && child.kind() != "named_argument" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
if text_of(name_node, code).as_deref() == Some(keyword_name) {
return true;
}
}
false
}
pub(super) fn extract_object_arg_property(
call_node: Node,
arg_index: usize,
prop_name: &str,
code: &[u8],
) -> Option<String> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let arg = args.named_children(&mut cursor).nth(arg_index)?;
let arg = unwrap_parens(arg);
if !matches!(arg.kind(), "object" | "dictionary") {
return None;
}
let mut c = arg.walk();
for child in arg.named_children(&mut c) {
if child.kind() != "pair" {
continue;
}
let Some(key_node) = child.child_by_field_name("key") else {
continue;
};
let key_text = match key_node.kind() {
"string" | "string_literal" => text_of(key_node, code).map(|raw| {
if raw.len() >= 2 {
raw[1..raw.len() - 1].to_string()
} else {
raw
}
}),
"computed_property_name" => continue,
_ => text_of(key_node, code),
};
if key_text.as_deref() != Some(prop_name) {
continue;
}
let val_node = child.child_by_field_name("value")?;
let val_node = unwrap_parens(val_node);
return match val_node.kind() {
"true" | "false" | "null" | "undefined" | "number" | "string" | "string_literal" => {
text_of(val_node, code).map(|s| s.to_string())
}
"identifier" => text_of(val_node, code)
.filter(|s| matches!(s.as_str(), "true" | "false" | "null" | "undefined")),
_ => None,
};
}
None
}
pub(super) fn has_object_arg_property(
call_node: Node,
arg_index: usize,
prop_name: &str,
code: &[u8],
) -> bool {
let Some(args) = call_node.child_by_field_name("arguments") else {
return false;
};
let mut cursor = args.walk();
let Some(arg) = args.named_children(&mut cursor).nth(arg_index) else {
return false;
};
let arg = unwrap_parens(arg);
if !matches!(arg.kind(), "object" | "dictionary") {
return false;
}
let mut c = arg.walk();
for child in arg.named_children(&mut c) {
match child.kind() {
"shorthand_property_identifier" | "shorthand_property_identifier_pattern"
if text_of(child, code).as_deref() == Some(prop_name) =>
{
return true;
}
"pair" => {
if let Some(key_node) = child.child_by_field_name("key") {
let key_text = match key_node.kind() {
"string" | "string_literal" => text_of(key_node, code).map(|raw| {
if raw.len() >= 2 {
raw[1..raw.len() - 1].to_string()
} else {
raw
}
}),
"computed_property_name" => continue,
_ => text_of(key_node, code),
};
if key_text.as_deref() == Some(prop_name) {
return true;
}
}
}
_ => {}
}
}
false
}
pub(super) fn arg0_kind_and_interpolation(call_node: Node) -> Option<(String, bool)> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let arg0 = args.named_children(&mut cursor).next()?;
let arg0 = unwrap_parens(arg0);
let kind = arg0.kind().to_string();
let has_interp = subtree_has_interpolation(arg0);
Some((kind, has_interp))
}
pub(super) fn java_chain_arg0_kind_for_method(
expr: Node,
target_methods: &[&str],
code: &[u8],
) -> Option<String> {
let n = unwrap_parens(expr);
if n.kind() == "method_invocation"
&& let Some(name_node) = n.child_by_field_name("name")
&& let Some(name) = text_of(name_node, code)
&& target_methods.iter().any(|m| *m == name)
{
let args = n.child_by_field_name("arguments")?;
let mut cursor = args.walk();
let arg0 = args.named_children(&mut cursor).next()?;
let arg0 = unwrap_parens(arg0);
return Some(arg0.kind().to_string());
}
if n.kind() == "method_invocation"
&& let Some(recv) = n.child_by_field_name("object")
&& let Some(found) = java_chain_arg0_kind_for_method(recv, target_methods, code)
{
return Some(found);
}
None
}
pub(super) fn ruby_chain_arg0_for_method(
expr: Node,
target_methods: &[&str],
code: &[u8],
) -> Option<(String, bool)> {
let n = unwrap_parens(expr);
if n.kind() == "call"
&& let Some(method) = n.child_by_field_name("method")
&& let Some(name) = text_of(method, code)
&& target_methods.iter().any(|m| *m == name)
{
return arg0_kind_and_interpolation(n);
}
if n.kind() == "call"
&& let Some(recv) = n
.child_by_field_name("receiver")
.or_else(|| n.child_by_field_name("object"))
&& let Some(found) = ruby_chain_arg0_for_method(recv, target_methods, code)
{
return Some(found);
}
let mut cursor = n.walk();
for c in n.named_children(&mut cursor) {
if let Some(found) = ruby_chain_arg0_for_method(c, target_methods, code) {
return Some(found);
}
}
None
}
fn subtree_has_interpolation(n: Node) -> bool {
if n.kind() == "interpolation" || n.kind() == "string_interpolation" {
return true;
}
let mut cursor = n.walk();
n.named_children(&mut cursor).any(subtree_has_interpolation)
}
pub(super) fn js_chain_arg0_kind_for_method(
expr: Node,
target_methods: &[&str],
code: &[u8],
) -> Option<(String, bool)> {
let n = unwrap_parens(expr);
if n.kind() == "call_expression" {
if let Some(function) = n.child_by_field_name("function") {
let prop_text = function
.child_by_field_name("property")
.and_then(|p| text_of(p, code));
let full_text = text_of(function, code);
let leaf_text = full_text
.as_ref()
.map(|s| s.rsplit('.').next().unwrap_or(s).to_string());
let matched = target_methods.iter().any(|m| {
prop_text.as_deref() == Some(*m)
|| leaf_text.as_deref() == Some(*m)
|| full_text.as_deref() == Some(*m)
|| full_text
.as_deref()
.is_some_and(|s| s.ends_with(&format!(".{m}")))
});
if matched {
return arg0_kind_and_interpolation(n);
}
if let Some(object) = function.child_by_field_name("object")
&& let Some(found) = js_chain_arg0_kind_for_method(object, target_methods, code)
{
return Some(found);
}
}
}
None
}
pub(super) fn js_chain_outer_method_for_inner<'a>(
outer: Node<'a>,
target_inner: &[&str],
code: &'a [u8],
) -> Option<String> {
let n = unwrap_parens(outer);
if n.kind() != "call_expression" {
return None;
}
let function = n.child_by_field_name("function")?;
let object = function.child_by_field_name("object")?;
if object.kind() == "call_expression" {
let inner_function = object.child_by_field_name("function");
if let Some(inner_function) = inner_function {
let prop_text = inner_function
.child_by_field_name("property")
.and_then(|p| text_of(p, code));
let full_text = text_of(inner_function, code);
let leaf_text = full_text
.as_ref()
.map(|s| s.rsplit('.').next().unwrap_or(s).to_string());
let inner_matched = target_inner.iter().any(|m| {
prop_text.as_deref() == Some(*m)
|| leaf_text.as_deref() == Some(*m)
|| full_text.as_deref() == Some(*m)
|| full_text
.as_deref()
.is_some_and(|s| s.ends_with(&format!(".{m}")))
});
if inner_matched {
return function
.child_by_field_name("property")
.and_then(|p| text_of(p, code).map(|s| s.to_string()));
}
}
return js_chain_outer_method_for_inner(object, target_inner, code);
}
None
}
pub(super) fn find_chained_inner_call<'a>(
outer: Node<'a>,
lang: &str,
code: &[u8],
) -> Option<(Node<'a>, String)> {
if !matches!(lookup(lang, outer.kind()), Kind::CallFn | Kind::CallMethod) {
return None;
}
let function = outer
.child_by_field_name("function")
.or_else(|| outer.child_by_field_name("method"))?;
if matches!(
lookup(lang, function.kind()),
Kind::CallFn | Kind::CallMethod
) {
if let Some(inner) = find_chained_inner_call(function, lang, code) {
return Some(inner);
}
let inner_func = function
.child_by_field_name("function")
.or_else(|| function.child_by_field_name("method"))
.or_else(|| function.child_by_field_name("name"))?;
let raw = text_of(inner_func, code)?;
let inner_text: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
return Some((function, inner_text));
}
let object = function.child_by_field_name("object")?;
if !matches!(lookup(lang, object.kind()), Kind::CallFn | Kind::CallMethod) {
return None;
}
if let Some(inner) = find_chained_inner_call(object, lang, code) {
return Some(inner);
}
let inner_func = object
.child_by_field_name("function")
.or_else(|| object.child_by_field_name("method"))
.or_else(|| object.child_by_field_name("name"))?;
let raw = text_of(inner_func, code)?;
let inner_text: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
Some((object, inner_text))
}
pub(super) fn walk_chain_inner_call_args<'a>(outer: Node<'a>, lang: &str, out: &mut Vec<Node<'a>>) {
if !matches!(lookup(lang, outer.kind()), Kind::CallFn | Kind::CallMethod) {
return;
}
let function = outer
.child_by_field_name("function")
.or_else(|| outer.child_by_field_name("method"));
let Some(function) = function else { return };
let object = function
.child_by_field_name("object")
.or_else(|| function.child_by_field_name("operand"))
.or_else(|| function.child_by_field_name("value"));
let Some(inner) = object else { return };
if !matches!(lookup(lang, inner.kind()), Kind::CallFn | Kind::CallMethod) {
return;
}
if let Some(args) = inner.child_by_field_name("arguments") {
let mut cursor = args.walk();
for arg in args.named_children(&mut cursor) {
out.push(arg);
}
}
walk_chain_inner_call_args(inner, lang, out);
}
pub(super) fn find_call_node_deep<'a>(n: Node<'a>, lang: &str, depth: u8) -> Option<Node<'a>> {
if depth == 0 {
return None;
}
match lookup(lang, n.kind()) {
Kind::CallFn | Kind::CallMethod | Kind::CallMacro => Some(n),
_ => {
let mut cursor = n.walk();
for c in n.children(&mut cursor) {
if let Some(found) = find_call_node_deep(c, lang, depth - 1) {
return Some(found);
}
}
None
}
}
}
pub(super) fn is_parameterized_query_call(call_node: Node, code: &[u8]) -> bool {
let Some(args) = call_node.child_by_field_name("arguments") else {
return false;
};
let mut cursor = args.walk();
let named: Vec<_> = args.named_children(&mut cursor).collect();
if named.len() < 2 {
return false;
}
let first_arg = named[0];
let query_text = match first_arg.kind() {
"string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal" => {
text_of(first_arg, code)
}
"template_string" => {
let mut c = first_arg.walk();
if first_arg
.named_children(&mut c)
.any(|ch| ch.kind() == "template_substitution")
{
return false; }
text_of(first_arg, code)
}
"concatenated_string" => {
text_of(first_arg, code)
}
_ => return false, };
let Some(qt) = query_text else {
return false;
};
has_sql_placeholders(&qt)
}
pub(super) fn has_sql_placeholders(s: &str) -> bool {
let bytes = s.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
match bytes[i] {
b'$' if i + 1 < len && bytes[i + 1].is_ascii_digit() && bytes[i + 1] != b'0' => {
return true;
}
b'?' => return true,
b'%' if i + 1 < len && bytes[i + 1] == b's' => {
return true;
}
b':' if i > 0
&& (bytes[i - 1] == b' '
|| bytes[i - 1] == b'='
|| bytes[i - 1] == b'('
|| bytes[i - 1] == b',')
&& i + 1 < len
&& bytes[i + 1].is_ascii_alphabetic() =>
{
return true;
}
_ => {}
}
i += 1;
}
false
}
#[allow(clippy::only_used_in_recursion)]
pub(super) fn is_syntactic_literal(node: Node, code: &[u8]) -> bool {
match node.kind() {
"string"
| "string_literal"
| "interpreted_string_literal"
| "raw_string_literal"
| "string_content"
| "string_fragment" => !has_string_interpolation(node),
"integer" | "integer_literal" | "int_literal" | "float" | "float_literal" | "number" => {
true
}
"true" | "false" | "null" | "nil" | "none" | "null_literal" | "boolean"
| "boolean_literal" => true,
"encapsed_string" => !has_interpolation_cfg(node),
"argument" => {
node.named_child_count() == 1
&& node
.named_child(0)
.is_some_and(|c| is_syntactic_literal(c, code))
}
"unary_expression" | "unary_op" => {
node.named_child_count() == 1
&& node
.named_child(0)
.is_some_and(|c| is_syntactic_literal(c, code))
}
"binary_expression" | "concatenated_string" => {
let count = node.named_child_count();
count >= 2
&& (0..count).all(|i| {
node.named_child(i as u32)
.is_some_and(|c| is_syntactic_literal(c, code))
})
}
"template_string" => {
let mut c = node.walk();
!node
.named_children(&mut c)
.any(|ch| ch.kind() == "template_substitution")
}
"list"
| "array"
| "array_expression"
| "array_creation_expression"
| "tuple"
| "tuple_expression" => {
let mut c = node.walk();
node.named_children(&mut c)
.all(|ch| is_syntactic_literal(ch, code))
}
"pair" => {
let mut c = node.walk();
node.named_children(&mut c)
.all(|ch| is_syntactic_literal(ch, code))
}
_ => false,
}
}
pub(super) fn has_string_interpolation(node: Node) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind().contains("interpolation") {
return true;
}
}
false
}
pub(super) fn has_interpolation_cfg(node: Node) -> bool {
for i in 0..node.child_count() as u32 {
if let Some(child) = node.child(i) {
let kind = child.kind();
if kind == "variable_name"
|| kind == "simple_variable"
|| kind.contains("interpolation")
{
return true;
}
}
}
false
}
pub(super) fn extract_literal_rhs(ast: Node, lang: &str, code: &[u8]) -> Option<String> {
use crate::labels::lookup;
let val_node = ast
.child_by_field_name("value")
.or_else(|| ast.child_by_field_name("right"));
if let Some(val) = val_node {
if is_syntactic_literal(val, code) {
return text_of(val, code);
}
}
if matches!(
lookup(lang, ast.kind()),
Kind::CallWrapper | Kind::Assignment
) {
let mut cursor = ast.walk();
for child in ast.children(&mut cursor) {
let child_val = child.child_by_field_name("value").or_else(|| {
if matches!(lookup(lang, child.kind()), Kind::Assignment) {
child.child_by_field_name("right")
} else {
None
}
});
if let Some(val) = child_val {
if is_syntactic_literal(val, code) {
return text_of(val, code);
}
}
}
}
if matches!(lookup(lang, ast.kind()), Kind::Return) {
let mut cursor = ast.walk();
for child in ast.named_children(&mut cursor) {
if is_syntactic_literal(child, code) {
return text_of(child, code);
}
}
}
None
}
pub(super) fn has_only_literal_args(call_node: Node, code: &[u8]) -> bool {
let Some(args) = call_node.child_by_field_name("arguments") else {
return false;
};
let mut cursor = args.walk();
let mut any_arg = false;
for ch in args.named_children(&mut cursor) {
any_arg = true;
if !is_syntactic_literal(ch, code) {
return false;
}
}
if !any_arg {
return false;
}
check_inner_call_args(call_node, code)
}
pub(super) fn check_inner_call_args(node: Node, code: &[u8]) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
let kind = child.kind();
if kind == "arguments" || kind == "argument_list" || kind == "actual_parameters" {
continue;
}
if child.child_by_field_name("arguments").is_some() {
if !has_only_literal_args(child, code) {
return false;
}
} else {
if !check_inner_call_args(child, code) {
return false;
}
}
}
true
}
pub(super) fn extract_rust_format_macro_named_idents(call_node: Node, code: &[u8]) -> Vec<String> {
if call_node.kind() != "macro_invocation" {
return Vec::new();
}
let Some(macro_node) = call_node.child_by_field_name("macro") else {
return Vec::new();
};
let Some(macro_text) = text_of(macro_node, code) else {
return Vec::new();
};
let leaf = macro_text
.rsplit("::")
.next()
.unwrap_or(macro_text.as_str());
if !is_rust_format_style_macro(leaf) {
return Vec::new();
}
let tt = match call_node.child_by_field_name("token_tree") {
Some(t) => t,
None => {
let mut cursor = call_node.walk();
match call_node
.children(&mut cursor)
.find(|c| c.kind() == "token_tree")
{
Some(t) => t,
None => return Vec::new(),
}
}
};
let mut cursor = tt.walk();
let fmt_lit = match tt
.children(&mut cursor)
.find(|c| matches!(c.kind(), "string_literal" | "raw_string_literal"))
{
Some(n) => n,
None => return Vec::new(),
};
let raw = match text_of(fmt_lit, code) {
Some(s) => s,
None => return Vec::new(),
};
let content = strip_literal_quotes(&raw, fmt_lit, code).unwrap_or_else(|| raw.clone());
parse_rust_format_named_idents(&content)
}
pub(super) fn extract_rust_format_macro_named_idents_in(n: Node, code: &[u8]) -> Vec<String> {
let mut out = Vec::new();
collect_format_macro_idents_recursive(n, code, &mut out, 0);
out
}
fn collect_format_macro_idents_recursive(n: Node, code: &[u8], out: &mut Vec<String>, depth: u32) {
if depth > 6 {
return;
}
if n.kind() == "macro_invocation" {
for ident in extract_rust_format_macro_named_idents(n, code) {
out.push(ident);
}
}
let mut cursor = n.walk();
for child in n.children(&mut cursor) {
collect_format_macro_idents_recursive(child, code, out, depth + 1);
}
}
fn is_rust_format_style_macro(name: &str) -> bool {
matches!(
name,
"format"
| "print"
| "println"
| "eprint"
| "eprintln"
| "write"
| "writeln"
| "panic"
| "format_args"
| "assert"
| "debug_assert"
| "todo"
| "unimplemented"
| "unreachable"
| "info"
| "warn"
| "error"
| "debug"
| "trace"
)
}
fn parse_rust_format_named_idents(s: &str) -> Vec<String> {
let bytes = s.as_bytes();
let mut out: Vec<String> = Vec::new();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'{' {
if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
i += 2;
continue;
}
let start = i + 1;
let mut j = start;
while j < bytes.len() && bytes[j] != b'}' && bytes[j] != b':' {
j += 1;
}
let ident_bytes = &bytes[start..j];
if is_valid_rust_format_ident(ident_bytes) {
if let Ok(name) = std::str::from_utf8(ident_bytes) {
out.push(name.to_string());
}
}
while j < bytes.len() && bytes[j] != b'}' {
j += 1;
}
i = j + 1;
} else if b == b'}' && i + 1 < bytes.len() && bytes[i + 1] == b'}' {
i += 2;
} else {
i += 1;
}
}
out
}
fn is_valid_rust_format_ident(b: &[u8]) -> bool {
if b.is_empty() {
return false;
}
let first = b[0];
if !(first.is_ascii_alphabetic() || first == b'_') {
return false;
}
if b.iter().all(|c| c.is_ascii_digit()) {
return false;
}
b.iter().all(|c| c.is_ascii_alphanumeric() || *c == b'_')
}
pub(super) fn extract_arg_uses(call_node: Node, code: &[u8]) -> Vec<Vec<String>> {
if call_node.kind() == "subshell" {
let mut result = Vec::new();
let mut cursor = call_node.walk();
for child in call_node.named_children(&mut cursor) {
if child.kind() == "interpolation" {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(child, code, &mut idents, &mut paths);
let mut combined = paths;
combined.extend(idents);
if !combined.is_empty() {
result.push(combined);
}
}
}
return result;
}
let Some(args_node) = call_node.child_by_field_name("arguments") else {
return Vec::new();
};
let mut result = Vec::new();
let mut cursor = args_node.walk();
for child in args_node.named_children(&mut cursor) {
let kind = child.kind();
if kind == "spread_element"
|| kind == "dictionary_splat"
|| kind == "list_splat"
|| kind == "splat_argument"
|| kind == "hash_splat_argument"
{
return Vec::new();
}
if kind == "keyword_argument" || kind == "named_argument" {
continue;
}
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(child, code, &mut idents, &mut paths);
let mut combined = paths;
combined.extend(idents);
result.push(combined);
}
result
}
pub(super) fn extract_kwargs(call_node: Node, code: &[u8]) -> Vec<(String, Vec<String>)> {
let Some(args_node) = call_node.child_by_field_name("arguments") else {
return Vec::new();
};
let mut out = Vec::new();
let mut cursor = args_node.walk();
for child in args_node.named_children(&mut cursor) {
let kind = child.kind();
if kind != "keyword_argument" && kind != "named_argument" {
continue;
}
let named_count = child.named_child_count();
let name_node = child
.child_by_field_name("name")
.or_else(|| child.named_child(0));
let value_node = child
.child_by_field_name("value")
.or_else(|| child.named_child(named_count.saturating_sub(1) as u32));
let (Some(nn), Some(vn)) = (name_node, value_node) else {
continue;
};
let Some(name) = text_of(nn, code) else {
continue;
};
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(vn, code, &mut idents, &mut paths);
let mut combined = paths;
combined.extend(idents);
out.push((name, combined));
}
out
}
pub(super) fn caps_stripped_by_literal_pattern(search: &str) -> Cap {
let mut caps = Cap::empty();
if search.contains("..") || search.contains('/') || search.contains('\\') {
caps |= Cap::FILE_IO;
}
if search.contains('<') || search.contains('>') {
caps |= Cap::HTML_ESCAPE;
}
if search.contains(';')
|| search.contains('|')
|| search.contains('&')
|| search.contains('$')
|| search.contains('`')
{
caps |= Cap::SHELL_ESCAPE;
}
if search.contains('\'') || search.contains('"') || search.contains("--") {
caps |= Cap::SQL_QUERY;
}
caps
}
const MAX_REPLACE_CHAIN_HOPS: usize = 16;
pub(super) fn detect_rust_replace_chain_sanitizer(call_ast: Node, code: &[u8]) -> Option<Cap> {
fn is_rust_str_literal(k: &str) -> bool {
matches!(k, "string_literal" | "raw_string_literal")
}
fn extract_rust_str_content<'a>(n: Node<'a>, code: &'a [u8]) -> Option<String> {
let mut cur = n.walk();
for c in n.named_children(&mut cur) {
if c.kind() == "string_content" {
return text_of(c, code);
}
}
let raw = text_of(n, code)?;
if raw.len() >= 2 {
Some(
raw.trim_start_matches('r')
.trim_start_matches('#')
.trim_end_matches('#')
.trim_matches('"')
.to_string(),
)
} else {
None
}
}
let mut current = call_ast;
let mut earned = Cap::empty();
for _ in 0..MAX_REPLACE_CHAIN_HOPS {
if current.kind() != "call_expression" {
if current.kind() == "identifier" && !earned.is_empty() {
return Some(earned);
}
return None;
}
let func = current.child_by_field_name("function")?;
if func.kind() != "field_expression" {
return None;
}
let method_ident = func.child_by_field_name("field")?;
let method_name = text_of(method_ident, code)?;
if method_name != "replace" && method_name != "replacen" {
return None;
}
let args_node = current.child_by_field_name("arguments")?;
let mut cursor = args_node.walk();
let positional: Vec<Node<'_>> = args_node
.named_children(&mut cursor)
.filter(|c| {
!matches!(
c.kind(),
"keyword_argument"
| "named_argument"
| "spread_element"
| "list_splat"
| "dictionary_splat"
| "splat_argument"
| "hash_splat_argument"
)
})
.collect();
let (arg0, arg1) = match positional.as_slice() {
[a, b, ..] => (*a, *b),
_ => return None,
};
if !is_rust_str_literal(arg0.kind()) || !is_rust_str_literal(arg1.kind()) {
return None;
}
let search = extract_rust_str_content(arg0, code)?;
let replacement = extract_rust_str_content(arg1, code)?;
if !caps_stripped_by_literal_pattern(&replacement).is_empty() {
return None;
}
earned |= caps_stripped_by_literal_pattern(&search);
current = func.child_by_field_name("value")?;
}
None
}
pub(super) fn detect_go_replace_call_sanitizer(call_ast: Node, code: &[u8]) -> Option<Cap> {
if call_ast.kind() != "call_expression" {
return None;
}
let func = call_ast.child_by_field_name("function")?;
if func.kind() != "selector_expression" {
return None;
}
let operand = func.child_by_field_name("operand")?;
if text_of(operand, code).as_deref() != Some("strings") {
return None;
}
let field = func.child_by_field_name("field")?;
let method_name = text_of(field, code)?;
if method_name != "Replace" && method_name != "ReplaceAll" {
return None;
}
let old_lit = extract_const_string_arg(call_ast, 1, code)?;
let new_lit = extract_const_string_arg(call_ast, 2, code)?;
if !caps_stripped_by_literal_pattern(&new_lit).is_empty() {
return None;
}
let caps = caps_stripped_by_literal_pattern(&old_lit);
if caps.is_empty() { None } else { Some(caps) }
}
pub(super) fn call_ident_of<'a>(n: Node<'a>, lang: &str, code: &'a [u8]) -> Option<String> {
if lang == "cpp" && n.kind() == "new_expression" {
return Some("new".to_string());
}
if lang == "cpp" && n.kind() == "delete_expression" {
return Some("delete".to_string());
}
match lookup(lang, n.kind()) {
Kind::Function => {
n.child_by_field_name("name")
.and_then(|nm| text_of(nm, code))
.or_else(|| Some(anon_fn_name(n.start_byte())))
}
Kind::CallFn => n
.child_by_field_name("function")
.or_else(|| n.child_by_field_name("method"))
.or_else(|| n.child_by_field_name("name"))
.or_else(|| n.child_by_field_name("type"))
.or_else(|| find_constructor_type_child(n))
.and_then(|f| {
let unwrapped = unwrap_parens(f);
if lookup(lang, unwrapped.kind()) == Kind::Function {
Some(anon_fn_name(unwrapped.start_byte()))
} else {
text_of(f, code)
}
}),
Kind::CallMethod => {
let func = n
.child_by_field_name("method")
.or_else(|| n.child_by_field_name("name"))
.and_then(|f| text_of(f, code));
let recv = n
.child_by_field_name("object")
.or_else(|| n.child_by_field_name("receiver"))
.or_else(|| n.child_by_field_name("scope"))
.and_then(|f| root_receiver_text(f, lang, code));
match (recv, func) {
(Some(r), Some(f)) => Some(format!("{r}.{f}")),
(_, Some(f)) => Some(f),
_ => None,
}
}
Kind::CallMacro => n
.child_by_field_name("macro")
.and_then(|f| text_of(f, code)),
_ => first_call_ident(n, lang, code),
}
}
pub(super) fn extract_arg_string_literals(call_node: Node, code: &[u8]) -> Vec<Option<String>> {
let Some(args_node) = call_node.child_by_field_name("arguments") else {
return Vec::new();
};
let mut result = Vec::new();
let mut cursor = args_node.walk();
for child in args_node.named_children(&mut cursor) {
let kind = child.kind();
if kind == "spread_element"
|| kind == "dictionary_splat"
|| kind == "list_splat"
|| kind == "splat_argument"
|| kind == "hash_splat_argument"
{
return Vec::new();
}
if kind == "keyword_argument" || kind == "named_argument" {
continue;
}
let target = if kind == "argument" {
child.named_child(0).unwrap_or(child)
} else {
child
};
let target_kind = target.kind();
let literal = match target_kind {
"string"
| "string_literal"
| "interpreted_string_literal"
| "raw_string_literal"
| "encapsed_string" => {
let raw = text_of(target, code);
raw.and_then(|s| strip_literal_quotes(&s, target, code))
}
_ => None,
};
result.push(literal);
}
result
}
pub(super) fn strip_literal_quotes(raw: &str, node: Node, code: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() == "string_content" {
return text_of(child, code).map(|s| s.to_string());
}
}
if raw.len() >= 2 {
let bytes = raw.as_bytes();
let first = bytes[0];
let last = bytes[raw.len() - 1];
if (first == b'"' && last == b'"') || (first == b'\'' && last == b'\'') {
return Some(raw[1..raw.len() - 1].to_string());
}
}
None
}
pub(super) fn extract_arg_callees(call_node: Node, lang: &str, code: &[u8]) -> Vec<Option<String>> {
let Some(args_node) = call_node.child_by_field_name("arguments") else {
return Vec::new();
};
let mut result = Vec::new();
let mut cursor = args_node.walk();
for child in args_node.named_children(&mut cursor) {
let kind = child.kind();
if kind == "spread_element"
|| kind == "dictionary_splat"
|| kind == "list_splat"
|| kind == "keyword_argument"
|| kind == "splat_argument"
|| kind == "hash_splat_argument"
|| kind == "named_argument"
{
return Vec::new();
}
result.push(call_ident_of(child, lang, code));
}
result
}
pub(super) fn def_use(
ast: Node,
lang: &str,
code: &[u8],
) -> (Option<String>, Vec<String>, Vec<String>) {
match lookup(lang, ast.kind()) {
Kind::CallWrapper => {
let mut defs = None;
let mut extra_defs = Vec::new();
let mut uses = Vec::new();
let def_node = ast
.child_by_field_name("pattern")
.or_else(|| ast.child_by_field_name("name"))
.or_else(|| ast.child_by_field_name("left"))
.or_else(|| {
ast.child_by_field_name("value")
.and_then(|v| v.child_by_field_name("alias"))
});
let val_node = ast
.child_by_field_name("value")
.or_else(|| ast.child_by_field_name("right"));
if def_node.is_some() || val_node.is_some() {
if let Some(pat) = def_node {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(pat, code, &mut idents, &mut paths);
let first = paths.pop().or_else(|| idents.first().cloned());
for ident in &idents {
if first.as_ref() != Some(ident) {
extra_defs.push(ident.clone());
}
}
defs = first;
}
if let Some(val) = val_node {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(val, code, &mut idents, &mut paths);
uses.extend(paths);
uses.extend(idents);
uses.extend(extract_rust_format_macro_named_idents_in(val, code));
}
} else {
let mut cursor = ast.walk();
for child in ast.children(&mut cursor) {
let is_assign = matches!(lookup(lang, child.kind()), Kind::Assignment);
let child_name = child
.child_by_field_name("name")
.or_else(|| child.child_by_field_name("declarator"))
.or_else(|| {
if is_assign {
child.child_by_field_name("left")
} else {
None
}
});
let child_value = child.child_by_field_name("value").or_else(|| {
if is_assign {
child.child_by_field_name("right")
} else {
None
}
});
if child_value.is_some() {
if let Some(name_node) = child_name
&& defs.is_none()
{
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(name_node, code, &mut idents, &mut paths);
let first = paths.pop().or_else(|| idents.first().cloned());
for ident in &idents {
if first.as_ref() != Some(ident) {
extra_defs.push(ident.clone());
}
}
defs = first;
}
if let Some(val_node) = child_value {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(val_node, code, &mut idents, &mut paths);
uses.extend(paths);
uses.extend(idents);
uses.extend(extract_rust_format_macro_named_idents_in(val_node, code));
}
}
}
if defs.is_none() && uses.is_empty() {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(ast, code, &mut idents, &mut paths);
uses.extend(paths);
uses.extend(idents);
uses.extend(extract_rust_format_macro_named_idents_in(ast, code));
}
}
(defs, uses, extra_defs)
}
Kind::Assignment => {
let mut defs = None;
let mut uses = Vec::new();
if let Some(lhs) = ast.child_by_field_name("left") {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(lhs, code, &mut idents, &mut paths);
defs = paths.pop().or_else(|| idents.pop());
}
if let Some(rhs) = ast.child_by_field_name("right") {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(rhs, code, &mut idents, &mut paths);
uses.extend(paths);
uses.extend(idents);
uses.extend(extract_rust_format_macro_named_idents_in(rhs, code));
}
(defs, uses, vec![])
}
Kind::If | Kind::While => {
let cond = ast.child_by_field_name("condition");
if let Some(c) = cond
&& c.kind() == "let_condition"
{
let mut defs = None;
let mut uses = Vec::new();
if let Some(pat) = c.child_by_field_name("pattern") {
let mut tmp = Vec::<String>::new();
collect_idents(pat, code, &mut tmp);
defs = tmp.into_iter().last();
}
if let Some(val) = c.child_by_field_name("value") {
collect_idents(val, code, &mut uses);
}
return (defs, uses, vec![]);
}
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(ast, code, &mut idents, &mut paths);
let mut uses = paths;
uses.extend(idents);
(None, uses, vec![])
}
Kind::For => {
let mut left = ast.child_by_field_name("left");
let mut right = ast.child_by_field_name("right");
if left.is_none() && right.is_none() {
let mut cursor = ast.walk();
for child in ast.children(&mut cursor) {
if child.kind() == "range_clause" {
left = child.child_by_field_name("left");
right = child.child_by_field_name("right");
break;
}
}
}
if left.is_none() && right.is_none() {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(ast, code, &mut idents, &mut paths);
let mut uses = paths;
uses.extend(idents);
return (None, uses, vec![]);
}
let mut defs: Option<String> = None;
let mut extra_defs: Vec<String> = Vec::new();
let mut uses: Vec<String> = Vec::new();
if let Some(pat) = left {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(pat, code, &mut idents, &mut paths);
let first = paths.pop().or_else(|| idents.first().cloned());
for ident in &idents {
if first.as_ref() != Some(ident) {
extra_defs.push(ident.clone());
}
}
defs = first;
}
if let Some(val) = right {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(val, code, &mut idents, &mut paths);
uses.extend(paths);
uses.extend(idents);
}
(defs, uses, extra_defs)
}
_ => {
let mut idents = Vec::new();
let mut paths = Vec::new();
collect_idents_with_paths(ast, code, &mut idents, &mut paths);
let mut uses = paths;
uses.extend(idents);
(None, uses, vec![])
}
}
}
#[derive(Debug, Clone)]
pub(super) struct ShellArrayMatch {
pub arg_position: usize,
pub payload_idents: Vec<String>,
}
pub(super) fn extract_shell_array_payload_idents(
call_node: Node,
code: &[u8],
) -> Vec<ShellArrayMatch> {
let mut out = Vec::new();
let Some(args_node) = call_node.child_by_field_name("arguments") else {
return out;
};
let mut cursor = args_node.walk();
for (idx, child) in args_node.named_children(&mut cursor).enumerate() {
let kind = child.kind();
if kind == "spread_element"
|| kind == "dictionary_splat"
|| kind == "list_splat"
|| kind == "splat_argument"
|| kind == "hash_splat_argument"
{
return Vec::new();
}
if kind == "keyword_argument" || kind == "named_argument" {
continue;
}
if let Some(idents) = shell_array_payload_idents_of(child, code) {
out.push(ShellArrayMatch {
arg_position: idx,
payload_idents: idents,
});
continue;
}
if matches!(kind, "object" | "dictionary") {
let mut cc = child.walk();
for pair in child.named_children(&mut cc) {
if pair.kind() != "pair" {
continue;
}
let Some(val_node) = pair.child_by_field_name("value") else {
continue;
};
let val_node = unwrap_parens(val_node);
if let Some(idents) = shell_array_payload_idents_of(val_node, code) {
out.push(ShellArrayMatch {
arg_position: idx,
payload_idents: idents,
});
}
}
}
}
out
}
fn shell_array_payload_idents_of(node: Node, code: &[u8]) -> Option<Vec<String>> {
let node = unwrap_parens(node);
if node.kind() != "array" {
return None;
}
let mut cursor = node.walk();
let elems: Vec<Node> = node.named_children(&mut cursor).collect();
if elems.len() < 3 {
return None;
}
let shell = const_string_value(elems[0], code)?;
if !is_known_shell(&shell) {
return None;
}
let flag = const_string_value(elems[1], code)?;
if !is_shell_command_flag(&shell, &flag) {
return None;
}
let mut idents: Vec<String> = Vec::new();
let mut paths: Vec<String> = Vec::new();
for elem in &elems[2..] {
collect_idents_with_paths(*elem, code, &mut idents, &mut paths);
}
let mut combined = paths;
combined.extend(idents);
let mut seen = std::collections::HashSet::new();
combined.retain(|s| seen.insert(s.clone()));
if combined.is_empty() {
return None;
}
Some(combined)
}
fn const_string_value(node: Node, code: &[u8]) -> Option<String> {
let node = unwrap_parens(node);
match node.kind() {
"string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal" => {
let raw = text_of(node, code)?;
if raw.len() >= 2 {
Some(raw[1..raw.len() - 1].to_string())
} else {
None
}
}
"template_string" => {
let mut c = node.walk();
if node
.named_children(&mut c)
.any(|ch| ch.kind() == "template_substitution")
{
return None;
}
let raw = text_of(node, code)?;
if raw.len() >= 2 {
Some(raw[1..raw.len() - 1].to_string())
} else {
None
}
}
_ => None,
}
}
fn is_known_shell(name: &str) -> bool {
let leaf = name.rsplit('/').next().unwrap_or(name);
matches!(
leaf,
"bash"
| "sh"
| "zsh"
| "dash"
| "ksh"
| "fish"
| "ash"
| "tcsh"
| "csh"
| "cmd"
| "cmd.exe"
| "powershell"
| "powershell.exe"
| "pwsh"
| "pwsh.exe"
)
}
fn is_shell_command_flag(shell: &str, flag: &str) -> bool {
let leaf = shell.rsplit('/').next().unwrap_or(shell);
let is_cmd = matches!(leaf, "cmd" | "cmd.exe");
let is_powershell = matches!(leaf, "powershell" | "powershell.exe" | "pwsh" | "pwsh.exe");
if is_cmd {
return matches!(flag, "/c" | "/C" | "/k" | "/K");
}
if is_powershell {
return matches!(
flag,
"-c" | "-Command" | "-command" | "-EncodedCommand" | "-encodedcommand"
);
}
flag == "-c"
}