use crate::analyze::macro_expand::{self, FunctionMacro};
use crate::utility::cert_c::ast_utils::get_node_text;
use lang_parsing_substrate::query;
use std::collections::{HashMap, HashSet};
use tree_sitter::Node;
const THREAD_SPAWN_APIS: &[(&str, usize, usize)] = &[
("pthread_create", 2, 4),
("thrd_create", 1, 3),
("CreateThread", 2, 6),
];
pub fn collect_concurrency_roots(
root: &Node,
source: &str,
function_macros: &HashMap<String, FunctionMacro>,
out: &mut HashSet<String>,
) {
for handler in lang_parsing_substrate::interrupt_handlers(*root, source) {
if let Some(name) = handler.name {
out.insert(name);
}
}
for call in query::find_descendants_of_kind(*root, "call_expression") {
let Some(function) = call.child_by_field_name("function") else {
continue;
};
if function.kind() != "identifier" {
continue;
}
let callee = get_node_text(&function, source);
let Some(args_node) = call.child_by_field_name("arguments") else {
continue;
};
let arg_texts = call_argument_texts(&args_node, source);
if let Some(name) = thread_entry_from_call(callee, &arg_texts) {
out.insert(name);
continue;
}
if let Some(name) = signal_handler_from_call(callee, &arg_texts) {
out.insert(name);
continue;
}
if let Some(expansion) =
macro_expand::expand_invocation(function_macros, callee, &arg_texts)
{
if let Some(name) = thread_entry_from_expansion(&expansion) {
out.insert(name);
} else if let Some(name) = signal_handler_from_expansion(&expansion) {
out.insert(name);
}
}
}
}
fn call_argument_texts(args_node: &Node, source: &str) -> Vec<String> {
let mut cursor = args_node.walk();
args_node
.named_children(&mut cursor)
.map(|n| get_node_text(&n, source).to_string())
.collect()
}
fn thread_entry_from_call(callee: &str, args: &[String]) -> Option<String> {
let &(_, idx, _) = THREAD_SPAWN_APIS
.iter()
.find(|(name, _, arity)| *name == callee && args.len() == *arity)?;
extract_identifier(args.get(idx)?)
}
fn signal_handler_from_call(callee: &str, args: &[String]) -> Option<String> {
if callee != "signal" || args.len() != 2 {
return None;
}
let handler = extract_identifier(&args[1])?;
if handler == "SIG_IGN" || handler == "SIG_DFL" {
return None;
}
Some(handler)
}
fn thread_entry_from_expansion(expansion: &str) -> Option<String> {
for &(name, idx, arity) in THREAD_SPAWN_APIS {
if let Some(args) = find_call_args_in_text(expansion, name) {
if args.len() == arity {
if let Some(id) = extract_identifier(&args[idx]) {
return Some(id);
}
}
}
}
None
}
fn signal_handler_from_expansion(expansion: &str) -> Option<String> {
let args = find_call_args_in_text(expansion, "signal")?;
if args.len() != 2 {
return None;
}
let handler = extract_identifier(&args[1])?;
if handler == "SIG_IGN" || handler == "SIG_DFL" {
return None;
}
Some(handler)
}
fn find_call_args_in_text(text: &str, callee: &str) -> Option<Vec<String>> {
let chars: Vec<char> = text.chars().collect();
let needle: Vec<char> = callee.chars().collect();
let mut i = 0;
while i + needle.len() < chars.len() {
let is_match = chars[i..i + needle.len()] == needle[..] && chars[i + needle.len()] == '(';
let boundary_ok = i == 0 || !is_ident_char(chars[i - 1]);
if is_match && boundary_ok {
let open = i + needle.len();
if let Some((args, _end)) = macro_expand::parse_call_args(&chars, open) {
return Some(args);
}
}
i += 1;
}
None
}
fn is_ident_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
pub fn reachable_from_roots(
roots: &HashSet<String>,
call_graph: &HashMap<String, HashSet<String>>,
ambiguous_call_targets: &HashSet<String>,
) -> HashSet<String> {
let mut reachable: HashSet<String> = HashSet::new();
let mut queue: Vec<String> = roots.iter().cloned().collect();
while let Some(current) = queue.pop() {
if !reachable.insert(current.clone()) {
continue;
}
if let Some(callees) = call_graph.get(¤t) {
for callee in callees {
if !ambiguous_call_targets.contains(callee) && !reachable.contains(callee) {
queue.push(callee.clone());
}
}
}
}
reachable
}
pub fn reachable_within_file(root: &Node, source: &str) -> HashSet<String> {
let function_macros = crate::analyze::macro_expand::collect_function_macros(root, source);
let mut call_graph: HashMap<String, HashSet<String>> = HashMap::new();
for edge in lang_parsing_substrate::calls::call_edges(*root, source) {
call_graph
.entry(edge.caller)
.or_default()
.insert(edge.callee);
}
let mut roots = HashSet::new();
collect_concurrency_roots(root, source, &function_macros, &mut roots);
reachable_from_roots(&roots, &call_graph, &HashSet::new())
}
fn extract_identifier(raw: &str) -> Option<String> {
let mut s = raw.trim();
loop {
let inner = s.strip_prefix('(').and_then(|s| s.strip_suffix(')'));
match inner {
Some(inner) if !inner.is_empty() => s = inner.trim(),
_ => break,
}
}
let s = s.strip_prefix('&').unwrap_or(s).trim();
if !s.is_empty()
&& s.chars()
.next()
.is_some_and(|c| c.is_alphabetic() || c == '_')
&& s.chars().all(is_ident_char)
{
Some(s.to_string())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_identifier_plain() {
assert_eq!(extract_identifier("worker"), Some("worker".to_string()));
}
#[test]
fn extract_identifier_address_of() {
assert_eq!(extract_identifier("&worker"), Some("worker".to_string()));
}
#[test]
fn extract_identifier_macro_wrapped() {
assert_eq!(
extract_identifier("(mosquitto__thread_main)"),
Some("mosquitto__thread_main".to_string())
);
}
#[test]
fn extract_identifier_rejects_field_expression() {
assert_eq!(extract_identifier("obj->cb"), None);
}
#[test]
fn extract_identifier_rejects_empty_parens() {
assert_eq!(extract_identifier("()"), None);
}
#[test]
fn thread_entry_direct_pthread_create() {
let args = vec![
"&t".to_string(),
"NULL".to_string(),
"worker".to_string(),
"arg".to_string(),
];
assert_eq!(
thread_entry_from_call("pthread_create", &args),
Some("worker".to_string())
);
}
#[test]
fn thread_entry_wrong_arity_not_matched() {
let args = vec!["&t".to_string(), "worker".to_string()];
assert_eq!(thread_entry_from_call("pthread_create", &args), None);
}
#[test]
fn signal_handler_direct() {
let args = vec!["SIGHUP".to_string(), "handle_signal".to_string()];
assert_eq!(
signal_handler_from_call("signal", &args),
Some("handle_signal".to_string())
);
}
#[test]
fn signal_handler_ignores_sig_ign() {
let args = vec!["SIGPIPE".to_string(), "SIG_IGN".to_string()];
assert_eq!(signal_handler_from_call("signal", &args), None);
}
#[test]
fn thread_entry_via_macro_expansion_mosquitto_shape() {
let expansion =
"pthread_create((&mosq->thread_id), (NULL), (mosquitto__thread_main), (mosq))";
assert_eq!(
thread_entry_from_expansion(expansion),
Some("mosquitto__thread_main".to_string())
);
}
#[test]
fn find_call_args_whole_word_boundary() {
let text = "my_pthread_create(a, b)";
assert_eq!(find_call_args_in_text(text, "pthread_create"), None);
}
}