use crate::graph::schema::CfgBlock;
use tree_sitter::{Node, Parser as TsParser};
mod control_flow;
mod extract;
#[cfg(test)]
mod tests;
use control_flow::*;
use extract::*;
pub fn extract_cfg_condition(node: &Node, source: &str) -> Option<String> {
let parent = node.parent()?;
let mut cursor = parent.walk();
let mut prev_attrs: Vec<Node> = Vec::new();
for sibling in parent.children(&mut cursor) {
if sibling.id() == node.id() {
break;
}
if sibling.kind() == "attribute_item" {
prev_attrs.push(sibling);
} else {
prev_attrs.clear();
}
}
for attr in prev_attrs.iter().rev() {
let cond = parse_cfg_attribute(attr, source);
if cond.is_some() {
return cond;
}
}
None
}
fn parse_cfg_attribute(attr_item: &Node, source: &str) -> Option<String> {
let mut attr_cursor = attr_item.walk();
for attr_child in attr_item.children(&mut attr_cursor) {
if attr_child.kind() != "attribute" {
continue;
}
let mut inner = attr_child.walk();
let first = attr_child.children(&mut inner).next()?;
if first.kind() != "identifier" {
continue;
}
let name = &source[first.start_byte()..first.end_byte()];
if name != "cfg" {
continue;
}
let mut inner2 = attr_child.walk();
for token_tree in attr_child.children(&mut inner2) {
if token_tree.kind() == "token_tree" {
let text = &source[token_tree.start_byte()..token_tree.end_byte()];
let trimmed = text.trim();
if trimmed.starts_with('(') && trimmed.ends_with(')') {
return Some(trimmed[1..trimmed.len() - 1].trim().to_string());
}
return Some(trimmed.to_string());
}
}
}
None
}
#[derive(Debug, Clone)]
pub struct CfgEdge {
pub source_idx: usize,
pub target_idx: usize,
pub edge_type: CfgEdgeType,
}
#[derive(Clone, Copy)]
pub(super) struct LoopScope {
pub header: Option<usize>,
pub exit: Option<usize>,
}
impl LoopScope {
pub const NONE: LoopScope = LoopScope {
header: None,
exit: None,
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CfgEdgeType {
Fallthrough,
ConditionalTrue,
ConditionalFalse,
Jump,
BackEdge,
Call,
Return,
}
impl CfgEdgeType {
pub fn as_str(&self) -> &'static str {
match self {
CfgEdgeType::Fallthrough => "fallthrough",
CfgEdgeType::ConditionalTrue => "conditional_true",
CfgEdgeType::ConditionalFalse => "conditional_false",
CfgEdgeType::Jump => "jump",
CfgEdgeType::BackEdge => "back_edge",
CfgEdgeType::Call => "call",
CfgEdgeType::Return => "return",
}
}
}
#[derive(Debug, Clone)]
pub struct CfgWithEdges {
pub blocks: Vec<CfgBlock>,
pub edges: Vec<CfgEdge>,
pub function_id: i64,
}
pub fn extract_cfg_with_edges(
source: &str,
function_id: i64,
language: tree_sitter::Language,
) -> CfgWithEdges {
let mut parser = TsParser::new();
if parser.set_language(&language).is_err() {
return CfgWithEdges {
function_id,
blocks: Vec::new(),
edges: Vec::new(),
};
}
let Some(tree) = parser.parse(source, None) else {
return CfgWithEdges {
function_id,
blocks: Vec::new(),
edges: Vec::new(),
};
};
let root = tree.root_node();
let func_node = find_function_node(&root);
if let Some(func) = func_node {
extract_cfg_from_function_node(&func, function_id, source)
} else {
extract_cfg_from_function_node(&root, function_id, source)
}
}
pub(super) fn find_function_node<'a>(root: &'a Node<'a>) -> Option<Node<'a>> {
let mut stack = vec![*root];
while let Some(node) = stack.pop() {
let kind = node.kind();
if kind == "function_item"
|| kind == "function_definition"
|| kind == "function_declaration"
|| kind == "method_declaration"
{
return Some(node);
}
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
stack.push(cursor.node());
if !cursor.goto_next_sibling() {
break;
}
}
}
}
None
}
pub fn extract_cfg_from_function_node(
func_node: &Node,
function_id: i64,
source: &str,
) -> CfgWithEdges {
let mut blocks = Vec::new();
let mut edges = Vec::new();
let cfg_condition = extract_cfg_condition(func_node, source);
let entry_block = create_entry_block(func_node, function_id, source);
blocks.push(entry_block);
if let Some(body) = find_function_body(func_node) {
let mut previous_block_idx: Option<usize> = Some(0); extract_blocks_from_node_with_fallthrough(
&body,
function_id,
source,
&mut blocks,
&mut edges,
&mut previous_block_idx,
LoopScope::NONE,
);
if blocks.len() > 1 && edges.iter().all(|e| e.source_idx != 0) {
edges.push(CfgEdge {
source_idx: 0,
target_idx: 1,
edge_type: CfgEdgeType::Fallthrough,
});
}
}
if let Some(ref cond) = cfg_condition {
for block in &mut blocks {
block.cfg_condition = Some(cond.clone());
}
}
CfgWithEdges {
blocks,
edges,
function_id,
}
}
pub(super) fn create_entry_block(func_node: &Node, function_id: i64, _source: &str) -> CfgBlock {
CfgBlock {
function_id,
kind: "entry".to_string(),
terminator: "fallthrough".to_string(),
byte_start: func_node.start_byte() as u64,
byte_end: func_node.end_byte() as u64,
start_line: func_node.start_position().row as u64 + 1,
start_col: func_node.start_position().column as u64,
end_line: func_node.end_position().row as u64 + 1,
end_col: func_node.end_position().column as u64,
cfg_hash: None,
statements: None,
cfg_condition: None,
}
}
pub(super) fn find_function_body<'a>(func_node: &'a Node) -> Option<Node<'a>> {
let mut cursor = func_node.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
let kind = child.kind();
if kind == "block" || kind == "compound_statement" {
return Some(child);
}
if !cursor.goto_next_sibling() {
break;
}
}
}
None
}
pub(super) fn create_block_from_node(
node: &Node,
function_id: i64,
_source: &str,
kind: &str,
terminator: &str,
) -> CfgBlock {
CfgBlock {
function_id,
kind: kind.to_string(),
terminator: terminator.to_string(),
byte_start: node.start_byte() as u64,
byte_end: node.end_byte() as u64,
start_line: node.start_position().row as u64 + 1,
start_col: node.start_position().column as u64,
end_line: node.end_position().row as u64 + 1,
end_col: node.end_position().column as u64,
cfg_hash: None,
statements: None,
cfg_condition: None,
}
}